Skip to content

Commit d886f50

Browse files
committed
sse-pass-http-req
1 parent 2ea1495 commit d886f50

File tree

20 files changed

+229
-49
lines changed

20 files changed

+229
-49
lines changed

.pre-commit-config.yaml

+20-6
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,29 @@ repos:
77
- id: prettier
88
types_or: [yaml, json5]
99

10-
- repo: https://github.com/astral-sh/ruff-pre-commit
11-
rev: v0.8.1
10+
- repo: local
1211
hooks:
1312
- id: ruff-format
13+
name: Ruff Format
14+
entry: uv run ruff
15+
args: [format]
16+
language: system
17+
types: [python]
18+
pass_filenames: false
1419
- id: ruff
15-
args: [--fix, --exit-non-zero-on-fix]
16-
17-
- repo: local
18-
hooks:
20+
name: Ruff
21+
entry: uv run ruff
22+
args: ["check", "--fix", "--exit-non-zero-on-fix"]
23+
types: [python]
24+
language: system
25+
pass_filenames: false
26+
- id: pyright
27+
name: pyright
28+
entry: uv run pyright
29+
args: [src]
30+
language: system
31+
types: [python]
32+
pass_filenames: false
1933
- id: uv-lock-check
2034
name: Check uv.lock is up to date
2135
entry: uv lock --check

README.md

+15-6
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,24 @@ The Model Context Protocol allows applications to provide context for LLMs in a
7373

7474
### Adding MCP to your python project
7575

76-
We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects. In a uv managed python project, add mcp to dependencies by:
76+
We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects.
7777

78-
```bash
79-
uv add "mcp[cli]"
80-
```
78+
If you haven't created a uv-managed project yet, create one:
79+
80+
```bash
81+
uv init mcp-server-demo
82+
cd mcp-server-demo
83+
```
84+
85+
Then add MCP to your project dependencies:
86+
87+
```bash
88+
uv add "mcp[cli]"
89+
```
8190

8291
Alternatively, for projects using pip for dependencies:
8392
```bash
84-
pip install mcp
93+
pip install "mcp[cli]"
8594
```
8695

8796
### Running the standalone MCP development tools
@@ -185,7 +194,7 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
185194
@mcp.tool()
186195
def query_db(ctx: Context) -> str:
187196
"""Tool that uses initialized resources"""
188-
db = ctx.request_context.lifespan_context["db"]
197+
db = ctx.request_context.lifespan_context.db
189198
return db.query()
190199
```
191200

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,10 @@ async def list_tools(self) -> list[Any]:
122122

123123
for item in tools_response:
124124
if isinstance(item, tuple) and item[0] == "tools":
125-
for tool in item[1]:
126-
tools.append(Tool(tool.name, tool.description, tool.inputSchema))
125+
tools.extend(
126+
Tool(tool.name, tool.description, tool.inputSchema)
127+
for tool in item[1]
128+
)
127129

128130
return tools
129131

@@ -282,10 +284,9 @@ def __init__(self, servers: list[Server], llm_client: LLMClient) -> None:
282284

283285
async def cleanup_servers(self) -> None:
284286
"""Clean up all servers properly."""
285-
cleanup_tasks = []
286-
for server in self.servers:
287-
cleanup_tasks.append(asyncio.create_task(server.cleanup()))
288-
287+
cleanup_tasks = [
288+
asyncio.create_task(server.cleanup()) for server in self.servers
289+
]
289290
if cleanup_tasks:
290291
try:
291292
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import sys
22

3-
from server import main
3+
from .server import main
44

55
sys.exit(main())
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import sys
22

3-
from server import main
3+
from .server import main
44

55
sys.exit(main())

pyproject.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ dependencies = [
2929
"starlette>=0.27",
3030
"sse-starlette>=1.6.1",
3131
"pydantic-settings>=2.5.2",
32-
"uvicorn>=0.23.1",
32+
"uvicorn>=0.23.1; sys_platform != 'emscripten'",
3333
]
3434

3535
[project.optional-dependencies]
@@ -89,8 +89,8 @@ venv = ".venv"
8989
strict = ["src/mcp/**/*.py"]
9090

9191
[tool.ruff.lint]
92-
select = ["E", "F", "I", "UP"]
93-
ignore = []
92+
select = ["C4", "E", "F", "I", "PERF", "UP"]
93+
ignore = ["PERF203"]
9494

9595
[tool.ruff]
9696
line-length = 88

src/mcp/client/__main__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ async def message_handler(
3838
async def run_session(
3939
read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception],
4040
write_stream: MemoryObjectSendStream[JSONRPCMessage],
41+
client_info: types.Implementation | None = None,
4142
):
4243
async with ClientSession(
43-
read_stream, write_stream, message_handler=message_handler
44+
read_stream,
45+
write_stream,
46+
message_handler=message_handler,
47+
client_info=client_info,
4448
) as session:
4549
logger.info("Initializing session")
4650
await session.initialize()

src/mcp/client/session.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from mcp.shared.session import BaseSession, RequestResponder
1111
from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
1212

13+
DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0")
14+
1315

1416
class SamplingFnT(Protocol):
1517
async def __call__(
@@ -97,6 +99,7 @@ def __init__(
9799
list_roots_callback: ListRootsFnT | None = None,
98100
logging_callback: LoggingFnT | None = None,
99101
message_handler: MessageHandlerFnT | None = None,
102+
client_info: types.Implementation | None = None,
100103
) -> None:
101104
super().__init__(
102105
read_stream,
@@ -105,6 +108,7 @@ def __init__(
105108
types.ServerNotification,
106109
read_timeout_seconds=read_timeout_seconds,
107110
)
111+
self._client_info = client_info or DEFAULT_CLIENT_INFO
108112
self._sampling_callback = sampling_callback or _default_sampling_callback
109113
self._list_roots_callback = list_roots_callback or _default_list_roots_callback
110114
self._logging_callback = logging_callback or _default_logging_callback
@@ -130,7 +134,7 @@ async def initialize(self) -> types.InitializeResult:
130134
experimental=None,
131135
roots=roots,
132136
),
133-
clientInfo=types.Implementation(name="mcp", version="0.1.0"),
137+
clientInfo=self._client_info,
134138
),
135139
)
136140
),

src/mcp/server/fastmcp/server.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import anyio
1717
import pydantic_core
18-
import uvicorn
1918
from pydantic import BaseModel, Field
2019
from pydantic.networks import AnyUrl
2120
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -466,6 +465,7 @@ async def run_stdio_async(self) -> None:
466465

467466
async def run_sse_async(self) -> None:
468467
"""Run the server using SSE transport."""
468+
import uvicorn
469469
starlette_app = self.sse_app()
470470

471471
config = uvicorn.Config(
@@ -491,6 +491,7 @@ async def handle_sse(request: Request) -> None:
491491
streams[0],
492492
streams[1],
493493
self._mcp_server.create_initialization_options(),
494+
extra_metadata={"http_request": request},
494495
)
495496

496497
return Starlette(
@@ -501,6 +502,15 @@ async def handle_sse(request: Request) -> None:
501502
],
502503
)
503504

505+
def get_http_request(self) -> Request | None:
506+
ctx = self.get_context()
507+
if (ctx.request_context and
508+
ctx.request_context.meta and
509+
hasattr(ctx.request_context.meta, "extra_metadata")):
510+
req: Request = ctx.request_context.meta.extra_metadata.get("http_request") # type: ignore
511+
return req
512+
return None
513+
504514
async def list_prompts(self) -> list[MCPPrompt]:
505515
"""List all available prompts."""
506516
prompts = self._prompt_manager.list_prompts()

src/mcp/server/fastmcp/tools/base.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import inspect
44
from collections.abc import Callable
5-
from typing import TYPE_CHECKING, Any
5+
from typing import TYPE_CHECKING, Any, get_origin
66

77
from pydantic import BaseModel, Field
88

@@ -53,7 +53,9 @@ def from_function(
5353
if context_kwarg is None:
5454
sig = inspect.signature(fn)
5555
for param_name, param in sig.parameters.items():
56-
if param.annotation is Context:
56+
if get_origin(param.annotation) is not None:
57+
continue
58+
if issubclass(param.annotation, Context):
5759
context_kwarg = param_name
5860
break
5961

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def model_dump_one_level(self) -> dict[str, Any]:
2727
That is, sub-models etc are not dumped - they are kept as pydantic models.
2828
"""
2929
kwargs: dict[str, Any] = {}
30-
for field_name in self.model_fields.keys():
30+
for field_name in self.__class__.model_fields.keys():
3131
kwargs[field_name] = getattr(self, field_name)
3232
return kwargs
3333

@@ -80,7 +80,7 @@ def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
8080
dicts (JSON objects) as JSON strings, which can be pre-parsed here.
8181
"""
8282
new_data = data.copy() # Shallow copy
83-
for field_name, _field_info in self.arg_model.model_fields.items():
83+
for field_name in self.arg_model.model_fields.keys():
8484
if field_name not in data.keys():
8585
continue
8686
if isinstance(data[field_name], str):

src/mcp/server/lowlevel/server.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ async def run(
479479
# but also make tracing exceptions much easier during testing and when using
480480
# in-process servers.
481481
raise_exceptions: bool = False,
482+
extra_metadata: dict[str, Any] | None = None,
482483
):
483484
async with AsyncExitStack() as stack:
484485
lifespan_context = await stack.enter_async_context(self.lifespan(self))
@@ -489,7 +490,8 @@ async def run(
489490
async with anyio.create_task_group() as tg:
490491
async for message in session.incoming_messages:
491492
logger.debug(f"Received message: {message}")
492-
493+
if hasattr(message, "request_meta") and getattr(message, "request_meta"):
494+
message.request_meta.extra_metadata = extra_metadata # type: ignore
493495
tg.start_soon(
494496
self._handle_message,
495497
message,

src/mcp/shared/memory.py

+3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import anyio
1111
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
1212

13+
import mcp.types as types
1314
from mcp.client.session import (
1415
ClientSession,
1516
ListRootsFnT,
@@ -65,6 +66,7 @@ async def create_connected_server_and_client_session(
6566
list_roots_callback: ListRootsFnT | None = None,
6667
logging_callback: LoggingFnT | None = None,
6768
message_handler: MessageHandlerFnT | None = None,
69+
client_info: types.Implementation | None = None,
6870
raise_exceptions: bool = False,
6971
) -> AsyncGenerator[ClientSession, None]:
7072
"""Creates a ClientSession that is connected to a running MCP server."""
@@ -95,6 +97,7 @@ async def create_connected_server_and_client_session(
9597
list_roots_callback=list_roots_callback,
9698
logging_callback=logging_callback,
9799
message_handler=message_handler,
100+
client_info=client_info,
98101
) as client_session:
99102
await client_session.initialize()
100103
yield client_session

src/mcp/types.py

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
class RequestParams(BaseModel):
4242
class Meta(BaseModel):
4343
progressToken: ProgressToken | None = None
44+
extra_metadata: dict[str, Any] | None = None
4445
"""
4546
If specified, the caller requests out-of-band progress notifications for
4647
this request (as represented by notifications/progress). The value of this

0 commit comments

Comments
 (0)