Skip to content

Commit 5ce136f

Browse files
authored
Merge branch 'main' into show-server-info
2 parents a3cfc48 + 568cbd1 commit 5ce136f

34 files changed

+469
-224
lines changed

README.md

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,36 @@
1616
<!-- omit in toc -->
1717
## Table of Contents
1818

19-
- [Overview](#overview)
20-
- [Installation](#installation)
21-
- [Quickstart](#quickstart)
22-
- [What is MCP?](#what-is-mcp)
23-
- [Core Concepts](#core-concepts)
24-
- [Server](#server)
25-
- [Resources](#resources)
26-
- [Tools](#tools)
27-
- [Prompts](#prompts)
28-
- [Images](#images)
29-
- [Context](#context)
30-
- [Running Your Server](#running-your-server)
31-
- [Development Mode](#development-mode)
32-
- [Claude Desktop Integration](#claude-desktop-integration)
33-
- [Direct Execution](#direct-execution)
34-
- [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)
35-
- [Examples](#examples)
36-
- [Echo Server](#echo-server)
37-
- [SQLite Explorer](#sqlite-explorer)
38-
- [Advanced Usage](#advanced-usage)
39-
- [Low-Level Server](#low-level-server)
40-
- [Writing MCP Clients](#writing-mcp-clients)
41-
- [MCP Primitives](#mcp-primitives)
42-
- [Server Capabilities](#server-capabilities)
43-
- [Documentation](#documentation)
44-
- [Contributing](#contributing)
45-
- [License](#license)
19+
- [MCP Python SDK](#mcp-python-sdk)
20+
- [Overview](#overview)
21+
- [Installation](#installation)
22+
- [Adding MCP to your python project](#adding-mcp-to-your-python-project)
23+
- [Running the standalone MCP development tools](#running-the-standalone-mcp-development-tools)
24+
- [Quickstart](#quickstart)
25+
- [What is MCP?](#what-is-mcp)
26+
- [Core Concepts](#core-concepts)
27+
- [Server](#server)
28+
- [Resources](#resources)
29+
- [Tools](#tools)
30+
- [Prompts](#prompts)
31+
- [Images](#images)
32+
- [Context](#context)
33+
- [Running Your Server](#running-your-server)
34+
- [Development Mode](#development-mode)
35+
- [Claude Desktop Integration](#claude-desktop-integration)
36+
- [Direct Execution](#direct-execution)
37+
- [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)
38+
- [Examples](#examples)
39+
- [Echo Server](#echo-server)
40+
- [SQLite Explorer](#sqlite-explorer)
41+
- [Advanced Usage](#advanced-usage)
42+
- [Low-Level Server](#low-level-server)
43+
- [Writing MCP Clients](#writing-mcp-clients)
44+
- [MCP Primitives](#mcp-primitives)
45+
- [Server Capabilities](#server-capabilities)
46+
- [Documentation](#documentation)
47+
- [Contributing](#contributing)
48+
- [License](#license)
4649

4750
[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg
4851
[pypi-url]: https://pypi.org/project/mcp/
@@ -143,8 +146,8 @@ The FastMCP server is your core interface to the MCP protocol. It handles connec
143146
```python
144147
# Add lifespan support for startup/shutdown with strong typing
145148
from contextlib import asynccontextmanager
149+
from collections.abc import AsyncIterator
146150
from dataclasses import dataclass
147-
from typing import AsyncIterator
148151

149152
from fake_database import Database # Replace with your actual DB type
150153

@@ -238,7 +241,8 @@ async def fetch_weather(city: str) -> str:
238241
Prompts are reusable templates that help LLMs interact with your server effectively:
239242

240243
```python
241-
from mcp.server.fastmcp import FastMCP, types
244+
from mcp.server.fastmcp import FastMCP
245+
from mcp.server.fastmcp.prompts import base
242246

243247
mcp = FastMCP("My App")
244248

@@ -249,11 +253,11 @@ def review_code(code: str) -> str:
249253

250254

251255
@mcp.prompt()
252-
def debug_error(error: str) -> list[types.Message]:
256+
def debug_error(error: str) -> list[base.Message]:
253257
return [
254-
types.UserMessage("I'm seeing this error:"),
255-
types.UserMessage(error),
256-
types.AssistantMessage("I'll help debug that. What have you tried so far?"),
258+
base.UserMessage("I'm seeing this error:"),
259+
base.UserMessage(error),
260+
base.AssistantMessage("I'll help debug that. What have you tried so far?"),
257261
]
258262
```
259263

@@ -441,7 +445,7 @@ For more control, you can use the low-level server implementation directly. This
441445

442446
```python
443447
from contextlib import asynccontextmanager
444-
from typing import AsyncIterator
448+
from collections.abc import AsyncIterator
445449

446450
from fake_database import Database # Replace with your actual DB type
447451

pyproject.toml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,11 @@ packages = ["src/mcp"]
7676
include = ["src/mcp", "tests"]
7777
venvPath = "."
7878
venv = ".venv"
79-
strict = [
80-
"src/mcp/server/fastmcp/tools/base.py",
81-
"src/mcp/client/*.py"
82-
]
79+
strict = ["src/mcp/**/*.py"]
80+
exclude = ["src/mcp/types.py"]
8381

8482
[tool.ruff.lint]
85-
select = ["E", "F", "I"]
83+
select = ["E", "F", "I", "UP"]
8684
ignore = []
8785

8886
[tool.ruff]

src/mcp/cli/claude.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import sys
66
from pathlib import Path
7+
from typing import Any
78

89
from mcp.server.fastmcp.utilities.logging import get_logger
910

@@ -116,10 +117,7 @@ def update_claude_config(
116117
# Add fastmcp run command
117118
args.extend(["mcp", "run", file_spec])
118119

119-
server_config = {
120-
"command": "uv",
121-
"args": args,
122-
}
120+
server_config: dict[str, Any] = {"command": "uv", "args": args}
123121

124122
# Add environment variables if specified
125123
if env_vars:

src/mcp/client/__main__.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import anyio
88
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
99

10+
import mcp.types as types
1011
from mcp.client.session import ClientSession
1112
from mcp.client.sse import sse_client
1213
from mcp.client.stdio import StdioServerParameters, stdio_client
14+
from mcp.shared.session import RequestResponder
1315
from mcp.types import JSONRPCMessage
1416

1517
if not sys.warnoptions:
@@ -21,26 +23,25 @@
2123
logger = logging.getLogger("client")
2224

2325

24-
async def receive_loop(session: ClientSession):
25-
logger.info("Starting receive loop")
26-
async for message in session.incoming_messages:
27-
if isinstance(message, Exception):
28-
logger.error("Error: %s", message)
29-
continue
26+
async def message_handler(
27+
message: RequestResponder[types.ServerRequest, types.ClientResult]
28+
| types.ServerNotification
29+
| Exception,
30+
) -> None:
31+
if isinstance(message, Exception):
32+
logger.error("Error: %s", message)
33+
return
3034

31-
logger.info("Received message from server: %s", message)
35+
logger.info("Received message from server: %s", message)
3236

3337

3438
async def run_session(
3539
read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception],
3640
write_stream: MemoryObjectSendStream[JSONRPCMessage],
3741
):
38-
async with (
39-
ClientSession(read_stream, write_stream) as session,
40-
anyio.create_task_group() as tg,
41-
):
42-
tg.start_soon(receive_loop, session)
43-
42+
async with ClientSession(
43+
read_stream, write_stream, message_handler=message_handler
44+
) as session:
4445
logger.info("Initializing session")
4546
await session.initialize()
4647
logger.info("Initialized")

src/mcp/client/session.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import timedelta
22
from typing import Any, Protocol
33

4+
import anyio.lowlevel
45
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
56
from pydantic import AnyUrl, TypeAdapter
67

@@ -24,6 +25,30 @@ async def __call__(
2425
) -> types.ListRootsResult | types.ErrorData: ...
2526

2627

28+
class LoggingFnT(Protocol):
29+
async def __call__(
30+
self,
31+
params: types.LoggingMessageNotificationParams,
32+
) -> None: ...
33+
34+
35+
class MessageHandlerFnT(Protocol):
36+
async def __call__(
37+
self,
38+
message: RequestResponder[types.ServerRequest, types.ClientResult]
39+
| types.ServerNotification
40+
| Exception,
41+
) -> None: ...
42+
43+
44+
async def _default_message_handler(
45+
message: RequestResponder[types.ServerRequest, types.ClientResult]
46+
| types.ServerNotification
47+
| Exception,
48+
) -> None:
49+
await anyio.lowlevel.checkpoint()
50+
51+
2752
async def _default_sampling_callback(
2853
context: RequestContext["ClientSession", Any],
2954
params: types.CreateMessageRequestParams,
@@ -43,6 +68,12 @@ async def _default_list_roots_callback(
4368
)
4469

4570

71+
async def _default_logging_callback(
72+
params: types.LoggingMessageNotificationParams,
73+
) -> None:
74+
pass
75+
76+
4677
ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(
4778
types.ClientResult | types.ErrorData
4879
)
@@ -64,6 +95,8 @@ def __init__(
6495
read_timeout_seconds: timedelta | None = None,
6596
sampling_callback: SamplingFnT | None = None,
6697
list_roots_callback: ListRootsFnT | None = None,
98+
logging_callback: LoggingFnT | None = None,
99+
message_handler: MessageHandlerFnT | None = None,
67100
) -> None:
68101
super().__init__(
69102
read_stream,
@@ -74,6 +107,8 @@ def __init__(
74107
)
75108
self._sampling_callback = sampling_callback or _default_sampling_callback
76109
self._list_roots_callback = list_roots_callback or _default_list_roots_callback
110+
self._logging_callback = logging_callback or _default_logging_callback
111+
self._message_handler = message_handler or _default_message_handler
77112

78113
async def initialize(self) -> types.InitializeResult:
79114
sampling = types.SamplingCapability()
@@ -321,3 +356,23 @@ async def _received_request(
321356
return await responder.respond(
322357
types.ClientResult(root=types.EmptyResult())
323358
)
359+
360+
async def _handle_incoming(
361+
self,
362+
req: RequestResponder[types.ServerRequest, types.ClientResult]
363+
| types.ServerNotification
364+
| Exception,
365+
) -> None:
366+
"""Handle incoming messages by forwarding to the message handler."""
367+
await self._message_handler(req)
368+
369+
async def _received_notification(
370+
self, notification: types.ServerNotification
371+
) -> None:
372+
"""Handle notifications from the server."""
373+
# Process specific notification types
374+
match notification.root:
375+
case types.LoggingMessageNotification(params=params):
376+
await self._logging_callback(params)
377+
case _:
378+
pass

src/mcp/client/stdio.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import os
22
import sys
33
from contextlib import asynccontextmanager
4-
from typing import Literal
4+
from pathlib import Path
5+
from typing import Literal, TextIO
56

67
import anyio
78
import anyio.lowlevel
@@ -66,6 +67,9 @@ class StdioServerParameters(BaseModel):
6667
If not specified, the result of get_default_environment() will be used.
6768
"""
6869

70+
cwd: str | Path | None = None
71+
"""The working directory to use when spawning the process."""
72+
6973
encoding: str = "utf-8"
7074
"""
7175
The text encoding used when sending/receiving messages to the server
@@ -83,7 +87,7 @@ class StdioServerParameters(BaseModel):
8387

8488

8589
@asynccontextmanager
86-
async def stdio_client(server: StdioServerParameters):
90+
async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stderr):
8791
"""
8892
Client transport for stdio: this will connect to a server by spawning a
8993
process and communicating with it over stdin/stdout.
@@ -99,8 +103,13 @@ async def stdio_client(server: StdioServerParameters):
99103

100104
process = await anyio.open_process(
101105
[server.command, *server.args],
102-
env=server.env if server.env is not None else get_default_environment(),
103-
stderr=sys.stderr,
106+
env=(
107+
{**get_default_environment(), **server.env}
108+
if server.env is not None
109+
else get_default_environment()
110+
),
111+
stderr=errlog,
112+
cwd=server.cwd,
104113
)
105114

106115
async def stdout_reader():

src/mcp/client/websocket.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import logging
3+
from collections.abc import AsyncGenerator
34
from contextlib import asynccontextmanager
4-
from typing import AsyncGenerator
55

66
import anyio
77
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream

src/mcp/server/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .fastmcp import FastMCP
22
from .lowlevel import NotificationOptions, Server
3+
from .models import InitializationOptions
34

4-
__all__ = ["Server", "FastMCP", "NotificationOptions"]
5+
__all__ = ["Server", "FastMCP", "NotificationOptions", "InitializationOptions"]

0 commit comments

Comments
 (0)