Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f85dc16

Browse files
authored
Merge branch 'main' into subprocess_kill_win32
2 parents b3e3aee + 6353dd1 commit f85dc16

File tree

32 files changed

+1547
-56
lines changed

32 files changed

+1547
-56
lines changed

.github/workflows/shared.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ jobs:
3737
run: uv run --no-sync pyright
3838

3939
test:
40-
runs-on: ubuntu-latest
40+
runs-on: ${{ matrix.os }}
4141
strategy:
4242
matrix:
4343
python-version: ["3.10", "3.11", "3.12", "3.13"]
44+
os: [ubuntu-latest, windows-latest]
4445

4546
steps:
4647
- uses: actions/checkout@v4
@@ -55,3 +56,4 @@ jobs:
5556

5657
- name: Run pytest
5758
run: uv run --no-sync pytest
59+
continue-on-error: true

README.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,8 @@ python server.py
387387
mcp run server.py
388388
```
389389

390+
Note that `mcp run` or `mcp dev` only supports server using FastMCP and not the low-level server variant.
391+
390392
### Streamable HTTP Transport
391393

392394
> **Note**: Streamable HTTP transport is superseding SSE transport for production deployments.
@@ -400,6 +402,9 @@ mcp = FastMCP("StatefulServer")
400402
# Stateless server (no session persistence)
401403
mcp = FastMCP("StatelessServer", stateless_http=True)
402404

405+
# Stateless server (no session persistence, no sse stream with supported client)
406+
mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True)
407+
403408
# Run server with streamable_http transport
404409
mcp.run(transport="streamable-http")
405410
```
@@ -432,15 +437,22 @@ def add_two(n: int) -> int:
432437

433438
```python
434439
# main.py
440+
import contextlib
435441
from fastapi import FastAPI
436442
from mcp.echo import echo
437443
from mcp.math import math
438444

439445

440-
app = FastAPI()
446+
# Create a combined lifespan to manage both session managers
447+
@contextlib.asynccontextmanager
448+
async def lifespan(app: FastAPI):
449+
async with contextlib.AsyncExitStack() as stack:
450+
await stack.enter_async_context(echo.mcp.session_manager.run())
451+
await stack.enter_async_context(math.mcp.session_manager.run())
452+
yield
441453

442-
# Use the session manager's lifespan
443-
app = FastAPI(lifespan=lambda app: echo.mcp.session_manager.run())
454+
455+
app = FastAPI(lifespan=lifespan)
444456
app.mount("/echo", echo.mcp.streamable_http_app())
445457
app.mount("/math", math.mcp.streamable_http_app())
446458
```
@@ -694,6 +706,8 @@ if __name__ == "__main__":
694706
asyncio.run(run())
695707
```
696708

709+
Caution: The `mcp run` and `mcp dev` tool doesn't support low-level server.
710+
697711
### Writing MCP Clients
698712

699713
The SDK provides a high-level client interface for connecting to MCP servers using various [transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports):

examples/clients/simple-chatbot/README.MD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ This example demonstrates how to integrate the Model Context Protocol (MCP) into
2525
```plaintext
2626
LLM_API_KEY=your_api_key_here
2727
```
28+
**Note:** The current implementation is configured to use the Groq API endpoint (`https://api.groq.com/openai/v1/chat/completions`) with the `llama-3.2-90b-vision-preview` model. If you plan to use a different LLM provider, you'll need to modify the `LLMClient` class in `main.py` to use the appropriate endpoint URL and model parameters.
2829

2930
3. **Configure servers:**
3031

examples/servers/simple-auth/mcp_simple_auth/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44

55
from mcp_simple_auth.server import main
66

7-
sys.exit(main())
7+
sys.exit(main()) # type: ignore[call-arg]

examples/servers/simple-prompt/mcp_simple_prompt/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from .server import main
44

5-
sys.exit(main())
5+
sys.exit(main()) # type: ignore[call-arg]

examples/servers/simple-resource/mcp_simple_resource/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from .server import main
44

5-
sys.exit(main())
5+
sys.exit(main()) # type: ignore[call-arg]

examples/servers/simple-resource/mcp_simple_resource/server.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import click
33
import mcp.types as types
44
from mcp.server.lowlevel import Server
5-
from pydantic import FileUrl
5+
from pydantic import AnyUrl
66

77
SAMPLE_RESOURCES = {
88
"greeting": "Hello! This is a sample text resource.",
@@ -26,7 +26,7 @@ def main(port: int, transport: str) -> int:
2626
async def list_resources() -> list[types.Resource]:
2727
return [
2828
types.Resource(
29-
uri=FileUrl(f"file:///{name}.txt"),
29+
uri=AnyUrl(f"file:///{name}.txt"),
3030
name=name,
3131
description=f"A sample text resource named {name}",
3232
mimeType="text/plain",
@@ -35,7 +35,9 @@ async def list_resources() -> list[types.Resource]:
3535
]
3636

3737
@app.read_resource()
38-
async def read_resource(uri: FileUrl) -> str | bytes:
38+
async def read_resource(uri: AnyUrl) -> str | bytes:
39+
if uri.path is None:
40+
raise ValueError(f"Invalid resource path: {uri}")
3941
name = uri.path.replace(".txt", "").lstrip("/")
4042

4143
if name not in SAMPLE_RESOURCES:
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from .server import main
22

33
if __name__ == "__main__":
4-
main()
4+
# Click will handle CLI arguments
5+
import sys
6+
7+
sys.exit(main()) # type: ignore[call-arg]
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .server import main
22

33
if __name__ == "__main__":
4-
main()
4+
main() # type: ignore[call-arg]

examples/servers/simple-tool/mcp_simple_tool/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
from .server import main
44

5-
sys.exit(main())
5+
sys.exit(main()) # type: ignore[call-arg]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Issues = "https://github.com/modelcontextprotocol/python-sdk/issues"
8686
packages = ["src/mcp"]
8787

8888
[tool.pyright]
89-
include = ["src/mcp", "tests"]
89+
include = ["src/mcp", "tests", "examples/servers"]
9090
venvPath = "."
9191
venv = ".venv"
9292
strict = ["src/mcp/**/*.py"]

src/mcp/cli/cli.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import subprocess
77
import sys
88
from pathlib import Path
9-
from typing import Annotated
9+
from typing import Annotated, Any
10+
11+
from mcp.server import FastMCP
12+
from mcp.server import Server as LowLevelServer
1013

1114
try:
1215
import typer
@@ -141,17 +144,48 @@ def _import_server(file: Path, server_object: str | None = None):
141144
module = importlib.util.module_from_spec(spec)
142145
spec.loader.exec_module(module)
143146

147+
def _check_server_object(server_object: Any, object_name: str):
148+
"""Helper function to check that the server object is supported
149+
150+
Args:
151+
server_object: The server object to check.
152+
153+
Returns:
154+
True if it's supported.
155+
"""
156+
if not isinstance(server_object, FastMCP):
157+
logger.error(
158+
f"The server object {object_name} is of type "
159+
f"{type(server_object)} (expecting {FastMCP})."
160+
)
161+
if isinstance(server_object, LowLevelServer):
162+
logger.warning(
163+
"Note that only FastMCP server is supported. Low level "
164+
"Server class is not yet supported."
165+
)
166+
return False
167+
return True
168+
144169
# If no object specified, try common server names
145170
if not server_object:
146171
# Look for the most common server object names
147172
for name in ["mcp", "server", "app"]:
148173
if hasattr(module, name):
174+
if not _check_server_object(getattr(module, name), f"{file}:{name}"):
175+
logger.error(
176+
f"Ignoring object '{file}:{name}' as it's not a valid "
177+
"server object"
178+
)
179+
continue
149180
return getattr(module, name)
150181

151182
logger.error(
152183
f"No server object found in {file}. Please either:\n"
153184
"1. Use a standard variable name (mcp, server, or app)\n"
154-
"2. Specify the object name with file:object syntax",
185+
"2. Specify the object name with file:object syntax"
186+
"3. If the server creates the FastMCP object within main() "
187+
" or another function, refactor the FastMCP object to be a "
188+
" global variable named mcp, server, or app.",
155189
extra={"file": str(file)},
156190
)
157191
sys.exit(1)
@@ -179,6 +213,9 @@ def _import_server(file: Path, server_object: str | None = None):
179213
)
180214
sys.exit(1)
181215

216+
if not _check_server_object(server, server_object):
217+
sys.exit(1)
218+
182219
return server
183220

184221

src/mcp/client/session.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import mcp.types as types
99
from mcp.shared.context import RequestContext
1010
from mcp.shared.message import SessionMessage
11-
from mcp.shared.session import BaseSession, RequestResponder
11+
from mcp.shared.session import BaseSession, ProgressFnT, RequestResponder
1212
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
1313

1414
DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0")
@@ -168,7 +168,11 @@ async def send_ping(self) -> types.EmptyResult:
168168
)
169169

170170
async def send_progress_notification(
171-
self, progress_token: str | int, progress: float, total: float | None = None
171+
self,
172+
progress_token: str | int,
173+
progress: float,
174+
total: float | None = None,
175+
message: str | None = None,
172176
) -> None:
173177
"""Send a progress notification."""
174178
await self.send_notification(
@@ -179,6 +183,7 @@ async def send_progress_notification(
179183
progressToken=progress_token,
180184
progress=progress,
181185
total=total,
186+
message=message,
182187
),
183188
),
184189
)
@@ -196,23 +201,29 @@ async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResul
196201
types.EmptyResult,
197202
)
198203

199-
async def list_resources(self) -> types.ListResourcesResult:
204+
async def list_resources(
205+
self, cursor: str | None = None
206+
) -> types.ListResourcesResult:
200207
"""Send a resources/list request."""
201208
return await self.send_request(
202209
types.ClientRequest(
203210
types.ListResourcesRequest(
204211
method="resources/list",
212+
cursor=cursor,
205213
)
206214
),
207215
types.ListResourcesResult,
208216
)
209217

210-
async def list_resource_templates(self) -> types.ListResourceTemplatesResult:
218+
async def list_resource_templates(
219+
self, cursor: str | None = None
220+
) -> types.ListResourceTemplatesResult:
211221
"""Send a resources/templates/list request."""
212222
return await self.send_request(
213223
types.ClientRequest(
214224
types.ListResourceTemplatesRequest(
215225
method="resources/templates/list",
226+
cursor=cursor,
216227
)
217228
),
218229
types.ListResourceTemplatesResult,
@@ -259,26 +270,32 @@ async def call_tool(
259270
name: str,
260271
arguments: dict[str, Any] | None = None,
261272
read_timeout_seconds: timedelta | None = None,
273+
progress_callback: ProgressFnT | None = None,
262274
) -> types.CallToolResult:
263-
"""Send a tools/call request."""
275+
"""Send a tools/call request with optional progress callback support."""
264276

265277
return await self.send_request(
266278
types.ClientRequest(
267279
types.CallToolRequest(
268280
method="tools/call",
269-
params=types.CallToolRequestParams(name=name, arguments=arguments),
281+
params=types.CallToolRequestParams(
282+
name=name,
283+
arguments=arguments,
284+
),
270285
)
271286
),
272287
types.CallToolResult,
273288
request_read_timeout_seconds=read_timeout_seconds,
289+
progress_callback=progress_callback,
274290
)
275291

276-
async def list_prompts(self) -> types.ListPromptsResult:
292+
async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResult:
277293
"""Send a prompts/list request."""
278294
return await self.send_request(
279295
types.ClientRequest(
280296
types.ListPromptsRequest(
281297
method="prompts/list",
298+
cursor=cursor,
282299
)
283300
),
284301
types.ListPromptsResult,
@@ -317,12 +334,13 @@ async def complete(
317334
types.CompleteResult,
318335
)
319336

320-
async def list_tools(self) -> types.ListToolsResult:
337+
async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
321338
"""Send a tools/list request."""
322339
return await self.send_request(
323340
types.ClientRequest(
324341
types.ListToolsRequest(
325342
method="tools/list",
343+
cursor=cursor,
326344
)
327345
),
328346
types.ListToolsResult,

src/mcp/client/stdio/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ async def stdin_writer():
181181
await terminate_windows_process(process)
182182
else:
183183
process.terminate()
184+
await read_stream.aclose()
185+
await write_stream.aclose()
184186

185187

186188
def _get_executable_command(command: str) -> str:

src/mcp/server/fastmcp/resources/types.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import httpx
1212
import pydantic
1313
import pydantic_core
14-
from pydantic import Field, ValidationInfo
14+
from pydantic import AnyUrl, Field, ValidationInfo, validate_call
1515

1616
from mcp.server.fastmcp.resources.base import Resource
1717

@@ -68,6 +68,31 @@ async def read(self) -> str | bytes:
6868
except Exception as e:
6969
raise ValueError(f"Error reading resource {self.uri}: {e}")
7070

71+
@classmethod
72+
def from_function(
73+
cls,
74+
fn: Callable[..., Any],
75+
uri: str,
76+
name: str | None = None,
77+
description: str | None = None,
78+
mime_type: str | None = None,
79+
) -> "FunctionResource":
80+
"""Create a FunctionResource from a function."""
81+
func_name = name or fn.__name__
82+
if func_name == "<lambda>":
83+
raise ValueError("You must provide a name for lambda functions")
84+
85+
# ensure the arguments are properly cast
86+
fn = validate_call(fn)
87+
88+
return cls(
89+
uri=AnyUrl(uri),
90+
name=func_name,
91+
description=description or fn.__doc__ or "",
92+
mime_type=mime_type or "text/plain",
93+
fn=fn,
94+
)
95+
7196

7297
class FileResource(Resource):
7398
"""A resource that reads from a file.

0 commit comments

Comments
 (0)