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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy