Skip to content

Commit c676189

Browse files
authored
Merge branch 'main' into FastMCP-and-structured-output
2 parents 66e7459 + a9cc822 commit c676189

File tree

21 files changed

+430
-72
lines changed

21 files changed

+430
-72
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
name: Comment on PRs in Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
pull-requests: write
9+
contents: read
10+
11+
jobs:
12+
comment-on-prs:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Get previous release
21+
id: previous_release
22+
uses: actions/github-script@v7
23+
with:
24+
script: |
25+
const currentTag = '${{ github.event.release.tag_name }}';
26+
27+
// Get all releases
28+
const { data: releases } = await github.rest.repos.listReleases({
29+
owner: context.repo.owner,
30+
repo: context.repo.repo,
31+
per_page: 100
32+
});
33+
34+
// Find current release index
35+
const currentIndex = releases.findIndex(r => r.tag_name === currentTag);
36+
37+
if (currentIndex === -1) {
38+
console.log('Current release not found in list');
39+
return null;
40+
}
41+
42+
// Get previous release (next in the list since they're sorted by date desc)
43+
const previousRelease = releases[currentIndex + 1];
44+
45+
if (!previousRelease) {
46+
console.log('No previous release found, this might be the first release');
47+
return null;
48+
}
49+
50+
console.log(`Found previous release: ${previousRelease.tag_name}`);
51+
52+
return previousRelease.tag_name;
53+
54+
- name: Get merged PRs between releases
55+
id: get_prs
56+
uses: actions/github-script@v7
57+
with:
58+
script: |
59+
const currentTag = '${{ github.event.release.tag_name }}';
60+
const previousTag = ${{ steps.previous_release.outputs.result }};
61+
62+
if (!previousTag) {
63+
console.log('No previous release found, skipping');
64+
return [];
65+
}
66+
67+
console.log(`Finding PRs between ${previousTag} and ${currentTag}`);
68+
69+
// Get commits between previous and current release
70+
const comparison = await github.rest.repos.compareCommits({
71+
owner: context.repo.owner,
72+
repo: context.repo.repo,
73+
base: previousTag,
74+
head: currentTag
75+
});
76+
77+
const commits = comparison.data.commits;
78+
console.log(`Found ${commits.length} commits`);
79+
80+
// Get PRs associated with each commit using GitHub API
81+
const prNumbers = new Set();
82+
83+
for (const commit of commits) {
84+
try {
85+
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
86+
owner: context.repo.owner,
87+
repo: context.repo.repo,
88+
commit_sha: commit.sha
89+
});
90+
91+
for (const pr of prs) {
92+
if (pr.merged_at) {
93+
prNumbers.add(pr.number);
94+
console.log(`Found merged PR: #${pr.number}`);
95+
}
96+
}
97+
} catch (error) {
98+
console.log(`Failed to get PRs for commit ${commit.sha}: ${error.message}`);
99+
}
100+
}
101+
102+
console.log(`Found ${prNumbers.size} merged PRs`);
103+
return Array.from(prNumbers);
104+
105+
- name: Comment on PRs
106+
uses: actions/github-script@v7
107+
with:
108+
script: |
109+
const prNumbers = ${{ steps.get_prs.outputs.result }};
110+
const releaseTag = '${{ github.event.release.tag_name }}';
111+
const releaseUrl = '${{ github.event.release.html_url }}';
112+
113+
const comment = `This pull request is included in [${releaseTag}](${releaseUrl})`;
114+
115+
let commentedCount = 0;
116+
117+
for (const prNumber of prNumbers) {
118+
try {
119+
// Check if we've already commented on this PR for this release
120+
const { data: comments } = await github.rest.issues.listComments({
121+
owner: context.repo.owner,
122+
repo: context.repo.repo,
123+
issue_number: prNumber,
124+
per_page: 100
125+
});
126+
127+
const alreadyCommented = comments.some(c =>
128+
c.user.type === 'Bot' && c.body.includes(releaseTag)
129+
);
130+
131+
if (alreadyCommented) {
132+
console.log(`Skipping PR #${prNumber} - already commented for ${releaseTag}`);
133+
continue;
134+
}
135+
136+
await github.rest.issues.createComment({
137+
owner: context.repo.owner,
138+
repo: context.repo.repo,
139+
issue_number: prNumber,
140+
body: comment
141+
});
142+
commentedCount++;
143+
console.log(`Successfully commented on PR #${prNumber}`);
144+
} catch (error) {
145+
console.error(`Failed to comment on PR #${prNumber}:`, error.message);
146+
}
147+
}
148+
149+
console.log(`Commented on ${commentedCount} of ${prNumbers.length} PRs`);

.github/workflows/main-checks.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- main
77
- "v*.*.*"
8+
- "v1.x"
89
tags:
910
- "v*.*.*"
1011

CONTRIBUTING.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ uv tool install pre-commit --with pre-commit-uv --force-reinstall
2323
## Development Workflow
2424

2525
1. Choose the correct branch for your changes:
26-
- For bug fixes to a released version: use the latest release branch (e.g. v1.1.x for 1.1.3)
27-
- For new features: use the main branch (which will become the next minor/major version)
28-
- If unsure, ask in an issue first
26+
27+
| Change Type | Target Branch | Example |
28+
|-------------|---------------|---------|
29+
| New features, breaking changes | `main` | New APIs, refactors |
30+
| Security fixes for v1 | `v1.x` | Critical patches |
31+
| Bug fixes for v1 | `v1.x` | Non-breaking fixes |
32+
33+
> **Note:** `main` is the v2 development branch. Breaking changes are welcome on `main`. The `v1.x` branch receives only security and critical bug fixes.
2934
3035
2. Create a new branch from your chosen base branch
3136

examples/clients/conformance-auth-client/mcp_conformance_auth_client/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import logging
3030
import os
3131
import sys
32-
from datetime import timedelta
3332
from urllib.parse import ParseResult, parse_qs, urlparse
3433

3534
import httpx
@@ -263,8 +262,8 @@ async def _run_session(server_url: str, oauth_auth: OAuthClientProvider) -> None
263262
async with streamablehttp_client(
264263
url=server_url,
265264
auth=oauth_auth,
266-
timeout=timedelta(seconds=30),
267-
sse_read_timeout=timedelta(seconds=60),
265+
timeout=30.0,
266+
sse_read_timeout=60.0,
268267
) as (read_stream, write_stream, _):
269268
async with ClientSession(read_stream, write_stream) as session:
270269
# Initialize the session

examples/clients/simple-auth-client/mcp_simple_auth_client/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
207207
async with sse_client(
208208
url=self.server_url,
209209
auth=oauth_auth,
210-
timeout=60,
210+
timeout=60.0,
211211
) as (read_stream, write_stream):
212212
await self._run_session(read_stream, write_stream, None)
213213
else:

src/mcp/client/auth/oauth2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ async def _exchange_token_authorization_code(
399399

400400
async def _handle_token_response(self, response: httpx.Response) -> None:
401401
"""Handle token exchange response."""
402-
if response.status_code != 200:
402+
if response.status_code not in {200, 201}:
403403
body = await response.aread() # pragma: no cover
404404
body_text = body.decode("utf-8") # pragma: no cover
405405
raise OAuthTokenError(f"Token exchange failed ({response.status_code}): {body_text}") # pragma: no cover

src/mcp/client/session.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from datetime import timedelta
32
from typing import Any, Protocol, overload
43

54
import anyio.lowlevel
@@ -113,7 +112,7 @@ def __init__(
113112
self,
114113
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
115114
write_stream: MemoryObjectSendStream[SessionMessage],
116-
read_timeout_seconds: timedelta | None = None,
115+
read_timeout_seconds: float | None = None,
117116
sampling_callback: SamplingFnT | None = None,
118117
elicitation_callback: ElicitationFnT | None = None,
119118
list_roots_callback: ListRootsFnT | None = None,
@@ -369,7 +368,7 @@ async def call_tool(
369368
self,
370369
name: str,
371370
arguments: dict[str, Any] | None = None,
372-
read_timeout_seconds: timedelta | None = None,
371+
read_timeout_seconds: float | None = None,
373372
progress_callback: ProgressFnT | None = None,
374373
*,
375374
meta: dict[str, Any] | None = None,

src/mcp/client/session_group.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import logging
1313
from collections.abc import Callable
1414
from dataclasses import dataclass
15-
from datetime import timedelta
1615
from types import TracebackType
1716
from typing import Any, TypeAlias, overload
1817

@@ -41,11 +40,11 @@ class SseServerParameters(BaseModel):
4140
# Optional headers to include in requests.
4241
headers: dict[str, Any] | None = None
4342

44-
# HTTP timeout for regular operations.
45-
timeout: float = 5
43+
# HTTP timeout for regular operations (in seconds).
44+
timeout: float = 5.0
4645

47-
# Timeout for SSE read operations.
48-
sse_read_timeout: float = 60 * 5
46+
# Timeout for SSE read operations (in seconds).
47+
sse_read_timeout: float = 300.0
4948

5049

5150
class StreamableHttpParameters(BaseModel):
@@ -57,11 +56,11 @@ class StreamableHttpParameters(BaseModel):
5756
# Optional headers to include in requests.
5857
headers: dict[str, Any] | None = None
5958

60-
# HTTP timeout for regular operations.
61-
timeout: timedelta = timedelta(seconds=30)
59+
# HTTP timeout for regular operations (in seconds).
60+
timeout: float = 30.0
6261

63-
# Timeout for SSE read operations.
64-
sse_read_timeout: timedelta = timedelta(seconds=60 * 5)
62+
# Timeout for SSE read operations (in seconds).
63+
sse_read_timeout: float = 300.0
6564

6665
# Close the client session when the transport closes.
6766
terminate_on_close: bool = True
@@ -76,7 +75,7 @@ class StreamableHttpParameters(BaseModel):
7675
class ClientSessionParameters:
7776
"""Parameters for establishing a client session to an MCP server."""
7877

79-
read_timeout_seconds: timedelta | None = None
78+
read_timeout_seconds: float | None = None
8079
sampling_callback: SamplingFnT | None = None
8180
elicitation_callback: ElicitationFnT | None = None
8281
list_roots_callback: ListRootsFnT | None = None
@@ -197,7 +196,7 @@ async def call_tool(
197196
self,
198197
name: str,
199198
arguments: dict[str, Any],
200-
read_timeout_seconds: timedelta | None = None,
199+
read_timeout_seconds: float | None = None,
201200
progress_callback: ProgressFnT | None = None,
202201
*,
203202
meta: dict[str, Any] | None = None,
@@ -210,7 +209,7 @@ async def call_tool(
210209
name: str,
211210
*,
212211
args: dict[str, Any],
213-
read_timeout_seconds: timedelta | None = None,
212+
read_timeout_seconds: float | None = None,
214213
progress_callback: ProgressFnT | None = None,
215214
meta: dict[str, Any] | None = None,
216215
) -> types.CallToolResult: ...
@@ -219,7 +218,7 @@ async def call_tool(
219218
self,
220219
name: str,
221220
arguments: dict[str, Any] | None = None,
222-
read_timeout_seconds: timedelta | None = None,
221+
read_timeout_seconds: float | None = None,
223222
progress_callback: ProgressFnT | None = None,
224223
*,
225224
meta: dict[str, Any] | None = None,
@@ -314,8 +313,8 @@ async def _establish_session(
314313
httpx_client = create_mcp_http_client(
315314
headers=server_params.headers,
316315
timeout=httpx.Timeout(
317-
server_params.timeout.total_seconds(),
318-
read=server_params.sse_read_timeout.total_seconds(),
316+
server_params.timeout,
317+
read=server_params.sse_read_timeout,
319318
),
320319
)
321320
await session_stack.enter_async_context(httpx_client)

src/mcp/client/sse.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ def _extract_session_id_from_endpoint(endpoint_url: str) -> str | None:
3131
async def sse_client(
3232
url: str,
3333
headers: dict[str, Any] | None = None,
34-
timeout: float = 5,
35-
sse_read_timeout: float = 60 * 5,
34+
timeout: float = 5.0,
35+
sse_read_timeout: float = 300.0,
3636
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
3737
auth: httpx.Auth | None = None,
3838
on_session_created: Callable[[str], None] | None = None,
@@ -46,8 +46,8 @@ async def sse_client(
4646
Args:
4747
url: The SSE endpoint URL.
4848
headers: Optional headers to include in requests.
49-
timeout: HTTP timeout for regular operations.
50-
sse_read_timeout: Timeout for SSE read operations.
49+
timeout: HTTP timeout for regular operations (in seconds).
50+
sse_read_timeout: Timeout for SSE read operations (in seconds).
5151
auth: Optional HTTPX authentication handler.
5252
on_session_created: Optional callback invoked with the session ID when received.
5353
"""

src/mcp/client/streamable_http.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ def __init__(
100100
self,
101101
url: str,
102102
headers: dict[str, str] | None = None,
103-
timeout: float | timedelta = 30,
104-
sse_read_timeout: float | timedelta = 60 * 5,
103+
timeout: float = 30.0,
104+
sse_read_timeout: float = 300.0,
105105
auth: httpx.Auth | None = None,
106106
) -> None: ...
107107

@@ -118,8 +118,8 @@ def __init__(
118118
Args:
119119
url: The endpoint URL.
120120
headers: Optional headers to include in requests.
121-
timeout: HTTP timeout for regular operations.
122-
sse_read_timeout: Timeout for SSE read operations.
121+
timeout: HTTP timeout for regular operations (in seconds).
122+
sse_read_timeout: Timeout for SSE read operations (in seconds).
123123
auth: Optional HTTPX authentication handler.
124124
"""
125125
# Check for deprecated parameters and issue runtime warning

0 commit comments

Comments
 (0)