Skip to content

Commit 1e4f570

Browse files
authored
Merge branch 'modelcontextprotocol:main' into feature/add-enable-disable-methods-tools
2 parents 213b1c3 + 6353dd1 commit 1e4f570

File tree

22 files changed

+1093
-39
lines changed

22 files changed

+1093
-39
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,9 @@ mcp = FastMCP("StatefulServer")
416416
# Stateless server (no session persistence)
417417
mcp = FastMCP("StatelessServer", stateless_http=True)
418418

419+
# Stateless server (no session persistence, no sse stream with supported client)
420+
mcp = FastMCP("StatelessServer", stateless_http=True, json_response=True)
421+
419422
# Run server with streamable_http transport
420423
mcp.run(transport="streamable-http")
421424
```
@@ -448,15 +451,22 @@ def add_two(n: int) -> int:
448451

449452
```python
450453
# main.py
454+
import contextlib
451455
from fastapi import FastAPI
452456
from mcp.echo import echo
453457
from mcp.math import math
454458

455459

456-
app = FastAPI()
460+
# Create a combined lifespan to manage both session managers
461+
@contextlib.asynccontextmanager
462+
async def lifespan(app: FastAPI):
463+
async with contextlib.AsyncExitStack() as stack:
464+
await stack.enter_async_context(echo.mcp.session_manager.run())
465+
await stack.enter_async_context(math.mcp.session_manager.run())
466+
yield
467+
457468

458-
# Use the session manager's lifespan
459-
app = FastAPI(lifespan=lambda app: echo.mcp.session_manager.run())
469+
app = FastAPI(lifespan=lifespan)
460470
app.mount("/echo", echo.mcp.streamable_http_app())
461471
app.mount("/math", math.mcp.streamable_http_app())
462472
```

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
@@ -85,7 +85,7 @@ Issues = "https://github.com/modelcontextprotocol/python-sdk/issues"
8585
packages = ["src/mcp"]
8686

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

src/mcp/client/session.py

Lines changed: 20 additions & 7 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")
@@ -201,23 +201,29 @@ async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResul
201201
types.EmptyResult,
202202
)
203203

204-
async def list_resources(self) -> types.ListResourcesResult:
204+
async def list_resources(
205+
self, cursor: str | None = None
206+
) -> types.ListResourcesResult:
205207
"""Send a resources/list request."""
206208
return await self.send_request(
207209
types.ClientRequest(
208210
types.ListResourcesRequest(
209211
method="resources/list",
212+
cursor=cursor,
210213
)
211214
),
212215
types.ListResourcesResult,
213216
)
214217

215-
async def list_resource_templates(self) -> types.ListResourceTemplatesResult:
218+
async def list_resource_templates(
219+
self, cursor: str | None = None
220+
) -> types.ListResourceTemplatesResult:
216221
"""Send a resources/templates/list request."""
217222
return await self.send_request(
218223
types.ClientRequest(
219224
types.ListResourceTemplatesRequest(
220225
method="resources/templates/list",
226+
cursor=cursor,
221227
)
222228
),
223229
types.ListResourceTemplatesResult,
@@ -264,26 +270,32 @@ async def call_tool(
264270
name: str,
265271
arguments: dict[str, Any] | None = None,
266272
read_timeout_seconds: timedelta | None = None,
273+
progress_callback: ProgressFnT | None = None,
267274
) -> types.CallToolResult:
268-
"""Send a tools/call request."""
275+
"""Send a tools/call request with optional progress callback support."""
269276

270277
return await self.send_request(
271278
types.ClientRequest(
272279
types.CallToolRequest(
273280
method="tools/call",
274-
params=types.CallToolRequestParams(name=name, arguments=arguments),
281+
params=types.CallToolRequestParams(
282+
name=name,
283+
arguments=arguments,
284+
),
275285
)
276286
),
277287
types.CallToolResult,
278288
request_read_timeout_seconds=read_timeout_seconds,
289+
progress_callback=progress_callback,
279290
)
280291

281-
async def list_prompts(self) -> types.ListPromptsResult:
292+
async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResult:
282293
"""Send a prompts/list request."""
283294
return await self.send_request(
284295
types.ClientRequest(
285296
types.ListPromptsRequest(
286297
method="prompts/list",
298+
cursor=cursor,
287299
)
288300
),
289301
types.ListPromptsResult,
@@ -322,12 +334,13 @@ async def complete(
322334
types.CompleteResult,
323335
)
324336

325-
async def list_tools(self) -> types.ListToolsResult:
337+
async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
326338
"""Send a tools/list request."""
327339
return await self.send_request(
328340
types.ClientRequest(
329341
types.ListToolsRequest(
330342
method="tools/list",
343+
cursor=cursor,
331344
)
332345
),
333346
types.ListToolsResult,

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.

src/mcp/server/fastmcp/server.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,11 @@ def __init__(
148148
self._mcp_server = MCPServer(
149149
name=name or "FastMCP",
150150
instructions=instructions,
151-
lifespan=lifespan_wrapper(self, self.settings.lifespan)
152-
if self.settings.lifespan
153-
else default_lifespan,
151+
lifespan=(
152+
lifespan_wrapper(self, self.settings.lifespan)
153+
if self.settings.lifespan
154+
else default_lifespan
155+
),
154156
)
155157
self._tool_manager = ToolManager(
156158
warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools
@@ -465,16 +467,16 @@ def decorator(fn: AnyFunction) -> AnyFunction:
465467
uri_template=uri,
466468
name=name,
467469
description=description,
468-
mime_type=mime_type or "text/plain",
470+
mime_type=mime_type,
469471
)
470472
else:
471473
# Register as regular resource
472-
resource = FunctionResource(
473-
uri=AnyUrl(uri),
474+
resource = FunctionResource.from_function(
475+
fn=fn,
476+
uri=uri,
474477
name=name,
475478
description=description,
476-
mime_type=mime_type or "text/plain",
477-
fn=fn,
479+
mime_type=mime_type,
478480
)
479481
self.add_resource(resource)
480482
return fn
@@ -961,7 +963,6 @@ async def report_progress(
961963
total: Optional total value e.g. 100
962964
message: Optional message e.g. Starting render...
963965
"""
964-
965966
progress_token = (
966967
self.request_context.meta.progressToken
967968
if self.request_context.meta

src/mcp/server/session.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ async def handle_list_prompts(ctx: RequestContext) -> list[types.Prompt]:
5252
BaseSession,
5353
RequestResponder,
5454
)
55+
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
5556

5657

5758
class InitializationState(Enum):
@@ -150,13 +151,16 @@ async def _received_request(
150151
):
151152
match responder.request.root:
152153
case types.InitializeRequest(params=params):
154+
requested_version = params.protocolVersion
153155
self._initialization_state = InitializationState.Initializing
154156
self._client_params = params
155157
with responder:
156158
await responder.respond(
157159
types.ServerResult(
158160
types.InitializeResult(
159-
protocolVersion=types.LATEST_PROTOCOL_VERSION,
161+
protocolVersion=requested_version
162+
if requested_version in SUPPORTED_PROTOCOL_VERSIONS
163+
else types.LATEST_PROTOCOL_VERSION,
160164
capabilities=self._init_options.capabilities,
161165
serverInfo=types.Implementation(
162166
name=self._init_options.server_name,

0 commit comments

Comments
 (0)