Skip to content

Commit 7b317cd

Browse files
authored
Merge branch 'main' into show-server-info
2 parents 9c8296a + 2ea1495 commit 7b317cd

File tree

11 files changed

+336
-33
lines changed

11 files changed

+336
-33
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Publish Docs manually
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
docs-publish:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
contents: write
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Configure Git Credentials
14+
run: |
15+
git config user.name github-actions[bot]
16+
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
17+
18+
- name: Install uv
19+
uses: astral-sh/setup-uv@v3
20+
with:
21+
enable-cache: true
22+
23+
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
24+
- uses: actions/cache@v4
25+
with:
26+
key: mkdocs-material-${{ env.cache_id }}
27+
path: .cache
28+
restore-keys: |
29+
mkdocs-material-
30+
31+
- run: uv sync --frozen --group docs
32+
- run: uv run --no-sync mkdocs gh-deploy --force

.github/workflows/publish-pypi.yml

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@ jobs:
1010
runs-on: ubuntu-latest
1111
needs: [checks]
1212
steps:
13-
- uses: actions/checkout@v4
13+
- uses: actions/checkout@v4
1414

15-
- name: Install uv
16-
uses: astral-sh/setup-uv@v3
17-
with:
18-
enable-cache: true
15+
- name: Install uv
16+
uses: astral-sh/setup-uv@v3
17+
with:
18+
enable-cache: true
1919

20-
- name: Set up Python 3.12
21-
run: uv python install 3.12
20+
- name: Set up Python 3.12
21+
run: uv python install 3.12
2222

23-
- name: Build
24-
run: uv build
23+
- name: Build
24+
run: uv build
2525

26-
- name: Upload artifacts
27-
uses: actions/upload-artifact@v4
28-
with:
29-
name: release-dists
30-
path: dist/
26+
- name: Upload artifacts
27+
uses: actions/upload-artifact@v4
28+
with:
29+
name: release-dists
30+
path: dist/
3131

3232
checks:
3333
uses: ./.github/workflows/shared.yml
@@ -39,17 +39,17 @@ jobs:
3939
needs:
4040
- release-build
4141
permissions:
42-
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
42+
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
4343

4444
steps:
45-
- name: Retrieve release distributions
46-
uses: actions/download-artifact@v4
47-
with:
48-
name: release-dists
49-
path: dist/
45+
- name: Retrieve release distributions
46+
uses: actions/download-artifact@v4
47+
with:
48+
name: release-dists
49+
path: dist/
5050

51-
- name: Publish package distributions to PyPI
52-
uses: pypa/gh-action-pypi-publish@release/v1
51+
- name: Publish package distributions to PyPI
52+
uses: pypa/gh-action-pypi-publish@release/v1
5353

5454
docs-publish:
5555
runs-on: ubuntu-latest
@@ -62,16 +62,19 @@ jobs:
6262
run: |
6363
git config user.name github-actions[bot]
6464
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
65-
- name: "Set up Python"
66-
uses: actions/setup-python@v5
65+
66+
- name: Install uv
67+
uses: astral-sh/setup-uv@v3
6768
with:
68-
python-version-file: ".python-version"
69+
enable-cache: true
70+
6971
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
7072
- uses: actions/cache@v4
7173
with:
7274
key: mkdocs-material-${{ env.cache_id }}
7375
path: .cache
7476
restore-keys: |
7577
mkdocs-material-
78+
7679
- run: uv sync --frozen --group docs
7780
- run: uv run --no-sync mkdocs gh-deploy --force
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
GROQ_API_KEY=gsk_1234567890
1+
LLM_API_KEY=gsk_1234567890

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ strict: true
55
repo_name: modelcontextprotocol/python-sdk
66
repo_url: https://github.com/modelcontextprotocol/python-sdk
77
edit_uri: edit/main/docs/
8+
site_url: https://modelcontextprotocol.github.io/python-sdk
89

910
# TODO(Marcelo): Add Anthropic copyright?
1011
# copyright: © Model Context Protocol 2025 to present

src/mcp/client/stdio.py renamed to src/mcp/client/stdio/__init__.py

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212

1313
import mcp.types as types
1414

15+
from .win32 import (
16+
create_windows_process,
17+
get_windows_executable_command,
18+
terminate_windows_process,
19+
)
20+
1521
# Environment variables to inherit by default
1622
DEFAULT_INHERITED_ENV_VARS = (
1723
[
@@ -101,14 +107,18 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
101107
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
102108
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
103109

104-
process = await anyio.open_process(
105-
[server.command, *server.args],
110+
command = _get_executable_command(server.command)
111+
112+
# Open process with stderr piped for capture
113+
process = await _create_platform_compatible_process(
114+
command=command,
115+
args=server.args,
106116
env=(
107117
{**get_default_environment(), **server.env}
108118
if server.env is not None
109119
else get_default_environment()
110120
),
111-
stderr=errlog,
121+
errlog=errlog,
112122
cwd=server.cwd,
113123
)
114124

@@ -159,4 +169,48 @@ async def stdin_writer():
159169
):
160170
tg.start_soon(stdout_reader)
161171
tg.start_soon(stdin_writer)
162-
yield read_stream, write_stream
172+
try:
173+
yield read_stream, write_stream
174+
finally:
175+
# Clean up process to prevent any dangling orphaned processes
176+
if sys.platform == "win32":
177+
await terminate_windows_process(process)
178+
else:
179+
process.terminate()
180+
181+
182+
def _get_executable_command(command: str) -> str:
183+
"""
184+
Get the correct executable command normalized for the current platform.
185+
186+
Args:
187+
command: Base command (e.g., 'uvx', 'npx')
188+
189+
Returns:
190+
str: Platform-appropriate command
191+
"""
192+
if sys.platform == "win32":
193+
return get_windows_executable_command(command)
194+
else:
195+
return command
196+
197+
198+
async def _create_platform_compatible_process(
199+
command: str,
200+
args: list[str],
201+
env: dict[str, str] | None = None,
202+
errlog: TextIO = sys.stderr,
203+
cwd: Path | str | None = None,
204+
):
205+
"""
206+
Creates a subprocess in a platform-compatible way.
207+
Returns a process handle.
208+
"""
209+
if sys.platform == "win32":
210+
process = await create_windows_process(command, args, env, errlog, cwd)
211+
else:
212+
process = await anyio.open_process(
213+
[command, *args], env=env, stderr=errlog, cwd=cwd
214+
)
215+
216+
return process

src/mcp/client/stdio/win32.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""
2+
Windows-specific functionality for stdio client operations.
3+
"""
4+
5+
import shutil
6+
import subprocess
7+
import sys
8+
from pathlib import Path
9+
from typing import TextIO
10+
11+
import anyio
12+
from anyio.abc import Process
13+
14+
15+
def get_windows_executable_command(command: str) -> str:
16+
"""
17+
Get the correct executable command normalized for Windows.
18+
19+
On Windows, commands might exist with specific extensions (.exe, .cmd, etc.)
20+
that need to be located for proper execution.
21+
22+
Args:
23+
command: Base command (e.g., 'uvx', 'npx')
24+
25+
Returns:
26+
str: Windows-appropriate command path
27+
"""
28+
try:
29+
# First check if command exists in PATH as-is
30+
if command_path := shutil.which(command):
31+
return command_path
32+
33+
# Check for Windows-specific extensions
34+
for ext in [".cmd", ".bat", ".exe", ".ps1"]:
35+
ext_version = f"{command}{ext}"
36+
if ext_path := shutil.which(ext_version):
37+
return ext_path
38+
39+
# For regular commands or if we couldn't find special versions
40+
return command
41+
except OSError:
42+
# Handle file system errors during path resolution
43+
# (permissions, broken symlinks, etc.)
44+
return command
45+
46+
47+
async def create_windows_process(
48+
command: str,
49+
args: list[str],
50+
env: dict[str, str] | None = None,
51+
errlog: TextIO = sys.stderr,
52+
cwd: Path | str | None = None,
53+
):
54+
"""
55+
Creates a subprocess in a Windows-compatible way.
56+
57+
Windows processes need special handling for console windows and
58+
process creation flags.
59+
60+
Args:
61+
command: The command to execute
62+
args: Command line arguments
63+
env: Environment variables
64+
errlog: Where to send stderr output
65+
cwd: Working directory for the process
66+
67+
Returns:
68+
A process handle
69+
"""
70+
try:
71+
# Try with Windows-specific flags to hide console window
72+
process = await anyio.open_process(
73+
[command, *args],
74+
env=env,
75+
# Ensure we don't create console windows for each process
76+
creationflags=subprocess.CREATE_NO_WINDOW # type: ignore
77+
if hasattr(subprocess, "CREATE_NO_WINDOW")
78+
else 0,
79+
stderr=errlog,
80+
cwd=cwd,
81+
)
82+
return process
83+
except Exception:
84+
# Don't raise, let's try to create the process without creation flags
85+
process = await anyio.open_process(
86+
[command, *args], env=env, stderr=errlog, cwd=cwd
87+
)
88+
return process
89+
90+
91+
async def terminate_windows_process(process: Process):
92+
"""
93+
Terminate a Windows process.
94+
95+
Note: On Windows, terminating a process with process.terminate() doesn't
96+
always guarantee immediate process termination.
97+
So we give it 2s to exit, or we call process.kill()
98+
which sends a SIGKILL equivalent signal.
99+
100+
Args:
101+
process: The process to terminate
102+
"""
103+
try:
104+
process.terminate()
105+
with anyio.fail_after(2.0):
106+
await process.wait()
107+
except TimeoutError:
108+
# Force kill if it doesn't terminate
109+
process.kill()

src/mcp/server/fastmcp/utilities/func_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
8888
pre_parsed = json.loads(data[field_name])
8989
except json.JSONDecodeError:
9090
continue # Not JSON - skip
91-
if isinstance(pre_parsed, str):
91+
if isinstance(pre_parsed, str | int | float):
9292
# This is likely that the raw value is e.g. `"hello"` which we
9393
# Should really be parsed as '"hello"' in Python - but if we parse
9494
# it as JSON it'll turn into just 'hello'. So we skip it.

src/mcp/server/lowlevel/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def create_content(data: str | bytes, mime_type: str | None):
301301

302302
return types.BlobResourceContents(
303303
uri=req.params.uri,
304-
blob=base64.urlsafe_b64encode(data).decode(),
304+
blob=base64.b64encode(data).decode(),
305305
mimeType=mime_type or "application/octet-stream",
306306
)
307307

0 commit comments

Comments
 (0)