Skip to content

Commit 200f47a

Browse files
committed
FastMCP working
1 parent e817d75 commit 200f47a

File tree

6 files changed

+78
-272
lines changed

6 files changed

+78
-272
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.venv/
22
node_modules/
33
src/vendor/
4-
.vscode/
4+
.vscode/
5+
.wrangler/

README.md

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
1-
# Vendoring Packages: FastAPI + Jinja2 Example
1+
# Python Workers: FastMCP Example
22

3-
*Note: You must have Python Packages enabled on your account for built-in packages to work. Request Access to our Closed Beta using [This Form](https://forms.gle/FcjjhV3YtPyjRPaL8)*
4-
5-
This is an example of a Python Worker that uses a built-in package (FastAPI) with a vendored package (Jinja2).
3+
This is an example of a Python Worker that uses the FastMCP package.
64

75
## Adding Packages
86

9-
Built-in packages can be selected from [this list](https://developers.cloudflare.com/workers/languages/python/packages/#supported-packages) and added to your `requirements.txt` file. These can be used with no other explicit install step.
10-
117
Vendored packages are added to your source files and need to be installed in a special manner. The Python Workers team plans to make this process automatic in the future, but for now, manual steps need to be taken.
128

139
### Vendoring Packages
1410

15-
[//]: # (NOTE: when updating the instructions below, be sure to also update the vendoring.yml CI workflow)
16-
1711
First, install Python3.12 and pip for Python 3.12.
1812

1913
*Currently, other versions of Python will not work - use 3.12!*
@@ -30,34 +24,11 @@ Within our virtual environment, install the pyodide CLI:
3024
.venv/bin/pyodide venv .venv-pyodide
3125
```
3226

33-
Next, add packages to your vendor.txt file. Here we'll add jinja2
34-
```
35-
jinja2
36-
```
37-
38-
Lastly, add these packages to your source files at `src/vendor`. For any additional packages, re-run this command.
27+
Lastly, download the vendored packages. For any additional packages, re-run this command.
3928
```console
4029
.venv-pyodide/bin/pip install -t src/vendor -r vendor.txt
4130
```
4231

43-
### Using Vendored packages
44-
45-
In your wrangler.toml, make the vendor directory available:
46-
47-
```toml
48-
[[rules]]
49-
globs = ["vendor/**"]
50-
type = "Data"
51-
fallthrough = true
52-
```
53-
54-
Now, you can import and use the packages:
55-
56-
```python
57-
import jinja2
58-
# ... etc ...
59-
```
60-
6132
### Developing and Deploying
6233

6334
To develop your Worker, run `npx wrangler@latest dev`.

src/asgi.py

Lines changed: 1 addition & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -1,159 +1,6 @@
11
from asyncio import Future, Event, Queue, ensure_future, sleep, create_task
22
from contextlib import contextmanager
33
from inspect import isawaitable
4-
import typing
5-
6-
if typing.TYPE_CHECKING:
7-
from typing import (
8-
Any,
9-
Callable,
10-
Literal,
11-
Optional,
12-
Protocol,
13-
TypedDict,
14-
Union,
15-
NotRequired,
16-
)
17-
from collections.abc import Awaitable, Iterable, MutableMapping
18-
19-
class HTTPRequestEvent(TypedDict):
20-
type: Literal["http.request"]
21-
body: bytes
22-
more_body: bool
23-
24-
class HTTPResponseDebugEvent(TypedDict):
25-
type: Literal["http.response.debug"]
26-
info: dict[str, object]
27-
28-
class HTTPResponseStartEvent(TypedDict):
29-
type: Literal["http.response.start"]
30-
status: int
31-
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
32-
trailers: NotRequired[bool]
33-
34-
class HTTPResponseBodyEvent(TypedDict):
35-
type: Literal["http.response.body"]
36-
body: bytes
37-
more_body: NotRequired[bool]
38-
39-
class HTTPResponseTrailersEvent(TypedDict):
40-
type: Literal["http.response.trailers"]
41-
headers: Iterable[tuple[bytes, bytes]]
42-
more_trailers: bool
43-
44-
class HTTPServerPushEvent(TypedDict):
45-
type: Literal["http.response.push"]
46-
path: str
47-
headers: Iterable[tuple[bytes, bytes]]
48-
49-
class HTTPDisconnectEvent(TypedDict):
50-
type: Literal["http.disconnect"]
51-
52-
class WebSocketConnectEvent(TypedDict):
53-
type: Literal["websocket.connect"]
54-
55-
class WebSocketAcceptEvent(TypedDict):
56-
type: Literal["websocket.accept"]
57-
subprotocol: NotRequired[str | None]
58-
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
59-
60-
class _WebSocketReceiveEventBytes(TypedDict):
61-
type: Literal["websocket.receive"]
62-
bytes: bytes
63-
text: NotRequired[None]
64-
65-
class _WebSocketReceiveEventText(TypedDict):
66-
type: Literal["websocket.receive"]
67-
bytes: NotRequired[None]
68-
text: str
69-
70-
WebSocketReceiveEvent = Union[
71-
_WebSocketReceiveEventBytes, _WebSocketReceiveEventText
72-
]
73-
74-
class _WebSocketSendEventBytes(TypedDict):
75-
type: Literal["websocket.send"]
76-
bytes: bytes
77-
text: NotRequired[None]
78-
79-
class _WebSocketSendEventText(TypedDict):
80-
type: Literal["websocket.send"]
81-
bytes: NotRequired[None]
82-
text: str
83-
84-
WebSocketSendEvent = Union[_WebSocketSendEventBytes, _WebSocketSendEventText]
85-
86-
class WebSocketResponseStartEvent(TypedDict):
87-
type: Literal["websocket.http.response.start"]
88-
status: int
89-
headers: Iterable[tuple[bytes, bytes]]
90-
91-
class WebSocketResponseBodyEvent(TypedDict):
92-
type: Literal["websocket.http.response.body"]
93-
body: bytes
94-
more_body: NotRequired[bool]
95-
96-
class WebSocketDisconnectEvent(TypedDict):
97-
type: Literal["websocket.disconnect"]
98-
code: int
99-
reason: NotRequired[str | None]
100-
101-
class WebSocketCloseEvent(TypedDict):
102-
type: Literal["websocket.close"]
103-
code: NotRequired[int]
104-
reason: NotRequired[str | None]
105-
106-
class LifespanStartupEvent(TypedDict):
107-
type: Literal["lifespan.startup"]
108-
109-
class LifespanShutdownEvent(TypedDict):
110-
type: Literal["lifespan.shutdown"]
111-
112-
class LifespanStartupCompleteEvent(TypedDict):
113-
type: Literal["lifespan.startup.complete"]
114-
115-
class LifespanStartupFailedEvent(TypedDict):
116-
type: Literal["lifespan.startup.failed"]
117-
message: str
118-
119-
class LifespanShutdownCompleteEvent(TypedDict):
120-
type: Literal["lifespan.shutdown.complete"]
121-
122-
class LifespanShutdownFailedEvent(TypedDict):
123-
type: Literal["lifespan.shutdown.failed"]
124-
message: str
125-
126-
WebSocketEvent = Union[
127-
WebSocketReceiveEvent, WebSocketDisconnectEvent, WebSocketConnectEvent
128-
]
129-
130-
ASGIReceiveEvent = Union[
131-
HTTPRequestEvent,
132-
HTTPDisconnectEvent,
133-
WebSocketConnectEvent,
134-
WebSocketReceiveEvent,
135-
WebSocketDisconnectEvent,
136-
LifespanStartupEvent,
137-
LifespanShutdownEvent,
138-
]
139-
140-
ASGISendEvent = Union[
141-
HTTPResponseStartEvent,
142-
HTTPResponseBodyEvent,
143-
HTTPResponseTrailersEvent,
144-
HTTPServerPushEvent,
145-
HTTPDisconnectEvent,
146-
WebSocketAcceptEvent,
147-
WebSocketSendEvent,
148-
WebSocketResponseStartEvent,
149-
WebSocketResponseBodyEvent,
150-
WebSocketCloseEvent,
151-
LifespanStartupCompleteEvent,
152-
LifespanStartupFailedEvent,
153-
LifespanShutdownCompleteEvent,
154-
LifespanShutdownFailedEvent,
155-
]
156-
1574

1585
ASGI = {"spec_version": "2.0", "version": "3.0"}
1596

@@ -274,14 +121,12 @@ async def process_request(app, req, env):
274121
await receive_queue.put({"body": b"", "more_body": False, "type": "http.request"})
275122

276123
async def receive():
277-
print("Receiving")
278124
message = None
279125
if not receive_queue.empty():
280126
message = await receive_queue.get()
281127
else:
282128
await finished_response.wait()
283129
message = {"type": "http.disconnect"}
284-
print(f"Received {message}")
285130
return message
286131

287132
# Create a transform stream for handling streaming responses
@@ -290,12 +135,11 @@ async def receive():
290135
writable = transform_stream.writable
291136
writer = writable.getWriter()
292137

293-
async def send(got: "ASGISendEvent"):
138+
async def send(got):
294139
nonlocal status
295140
nonlocal headers
296141
nonlocal is_sse
297142

298-
print(got)
299143
if got["type"] == "http.response.start":
300144
status = got["status"]
301145
# Like above, we need to convert byte-pairs into string explicitly.
@@ -305,7 +149,6 @@ async def send(got: "ASGISendEvent"):
305149
if k.lower() == "content-type" and v.lower().startswith(
306150
"text/event-stream"
307151
):
308-
print("SSE RESPONSE")
309152
is_sse = True
310153

311154
# For SSE, create and return the response immediately after http.response.start
@@ -318,7 +161,6 @@ async def send(got: "ASGISendEvent"):
318161
elif got["type"] == "http.response.body":
319162
body = got["body"]
320163
more_body = got.get("more_body", False)
321-
print(f"{body=}, {more_body=}")
322164

323165
# Convert body to JS buffer
324166
px = create_proxy(body)
@@ -367,7 +209,6 @@ async def run_app():
367209
# For non-SSE responses, we need to wait for the application to complete
368210
if not is_sse:
369211
await app_task
370-
print(f"Returning response! {is_sse}")
371212
return response
372213

373214

src/uvicorn.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# This file must exist as a hack to satisfy mcp.
2+
# mcp has an optional dependency on uvicorn but still imports it at the top scope, see:
3+
# https://github.com/modelcontextprotocol/python-sdk/blob/main/src/mcp/server/fastmcp/server.py#L18
4+
# Because we never call `run_sse_async` this is not required. However, Python workers used asgi.py
5+
# rather than uvicorn which is why this hack is needed. With this, the import succeeds.

0 commit comments

Comments
 (0)