Skip to content

Commit d98577f

Browse files
authored
Merge branch 'main' into main
2 parents 84ad76f + ff3ebcf commit d98577f

File tree

7 files changed

+68
-32
lines changed

7 files changed

+68
-32
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ To use this server, you must have both Python and [Deno](https://deno.com/) inst
3434
The server can be run with `deno` installed using `uvx`:
3535

3636
```bash
37-
uvx mcp-run-python [-h] [--version] [--port PORT] [--deps DEPS] {stdio,streamable-http,example}
37+
uvx mcp-run-python [-h] [--version] [--port PORT] [--deps DEPS] {stdio,streamable-http,streamable-http-stateless,example}
3838
```
3939

4040
where:
@@ -46,6 +46,8 @@ where:
4646
[Streamable HTTP MCP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) -
4747
suitable for running the server as an HTTP server to connect locally or remotely. This supports stateful requests, but
4848
does not require the client to hold a stateful connection like SSE
49+
- `streamable-http-stateless` runs the server with [Streamable HTTP MCP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http) in stateless mode and does not
50+
support server-to-client notifications
4951
- `example` will run a minimal Python script using `numpy`, useful for checking that the package is working, for the code
5052
to run successfully, you'll need to install `numpy` using `uvx mcp-run-python --deps numpy example`
5153

@@ -91,7 +93,6 @@ uv add mcp-run-python
9193

9294
With `mcp-run-python` installed, you can also run deno directly with `prepare_deno_env` or `async_prepare_deno_env`
9395

94-
9596
```python
9697
from pydantic_ai import Agent
9798
from pydantic_ai.mcp import MCPServerStdio

mcp_run_python/_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def cli_logic(args_list: Sequence[str] | None = None) -> int:
3030
parser.add_argument('--version', action='store_true', help='Show version and exit')
3131
parser.add_argument(
3232
'mode',
33-
choices=['stdio', 'streamable-http', 'example'],
33+
choices=['stdio', 'streamable-http', 'streamable-http-stateless', 'example'],
3434
nargs='?',
3535
help='Mode to run the server in.',
3636
)

mcp_run_python/deno/src/main.ts

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@ export async function main() {
3232
await runStdio(deps, flags['return-mode']);
3333
return;
3434
} else if (args[0] === 'streamable_http') {
35-
const port = parseInt(flags.port);
36-
const host = flags.host;
37-
runStreamableHttp(port, host, deps, flags['return-mode']);
38-
return;
35+
const port = parseInt(flags.port)
36+
const host = flags.host
37+
runStreamableHttp(port, host, deps, flags['return-mode'], false)
38+
return
39+
} else if (args[0] === 'streamable_http_stateless') {
40+
const port = parseInt(flags.port)
41+
const host = flags.host
42+
runStreamableHttp(port, host, deps, flags['return-mode'], true)
43+
return
3944
} else if (args[0] === 'example') {
4045
await example(deps);
4146
return;
@@ -48,7 +53,7 @@ export async function main() {
4853
`\
4954
Invalid arguments: ${args.join(' ')}
5055
51-
Usage: deno ... deno/main.ts [stdio|streamable_http|install_deps|noop]
56+
Usage: deno ... deno/main.ts [stdio|streamable_http|streamable_http_stateless|install_deps|noop]
5257
5358
options:
5459
--port <port> Port to run the HTTP server on (default: 3001)
@@ -197,19 +202,55 @@ function httpSetJsonResponse(
197202
/*
198203
* Run the MCP server using the Streamable HTTP transport
199204
*/
200-
function runStreamableHttp(
201-
port: number,
202-
host: string,
203-
deps: string[],
204-
returnMode: string
205-
) {
205+
function runStreamableHttp(port: number, host: string, deps: string[], returnMode: string, stateless: boolean): void {
206+
const server = (stateless ? createStatelessHttpServer : createStatefulHttpServer)(deps, returnMode)
207+
server.listen(port, host,() => {
208+
console.log(`Listening on host ${host} port ${port}`)
209+
})
210+
}
211+
212+
function createStatelessHttpServer(deps: string[], returnMode: string): http.Server {
213+
return http.createServer(async (req, res) => {
214+
const url = httpGetUrl(req)
215+
216+
if (url.pathname !== '/mcp') {
217+
httpSetTextResponse(res, 404, 'Page not found')
218+
return
219+
}
220+
221+
try {
222+
const mcpServer = createServer(deps, returnMode)
223+
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
224+
sessionIdGenerator: undefined,
225+
})
226+
227+
res.on('close', () => {
228+
transport.close()
229+
mcpServer.close()
230+
})
231+
232+
await mcpServer.connect(transport)
233+
234+
const body = req.method === 'POST' ? await httpGetBody(req) : undefined
235+
await transport.handleRequest(req, res, body)
236+
} catch (error) {
237+
console.error('Error handling MCP request:', error)
238+
if (!res.headersSent) {
239+
httpSetJsonResponse(res, 500, 'Internal server error', -32603)
240+
}
241+
}
242+
})
243+
}
244+
245+
function createStatefulHttpServer(deps: string[], returnMode: string): http.Server {
246+
// Stateful mode with session management
206247
// https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#with-session-management
207248
const mcpServer = createServer(deps, returnMode);
208249
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
209250

210-
const server = http.createServer(async (req, res) => {
211-
const url = httpGetUrl(req);
212-
let pathMatch = false;
251+
return http.createServer(async (req, res) => {
252+
const url = httpGetUrl(req)
253+
let pathMatch = false
213254
function match(method: string, path: string): boolean {
214255
if (url.pathname === path) {
215256
pathMatch = true;
@@ -282,13 +323,7 @@ function runStreamableHttp(
282323
} else {
283324
httpSetTextResponse(res, 404, 'Page not found');
284325
}
285-
});
286-
287-
server.listen(port, host, () => {
288-
console.log(
289-
`MCP Streamable HTTP server listening on http://${host}:${port}`
290-
);
291-
});
326+
})
292327
}
293328

294329
/*

mcp_run_python/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ def run_mcp_server(
5656
deps_log_handler=deps_log_handler,
5757
allow_networking=allow_networking,
5858
) as env:
59-
if mode == "streamable_http":
60-
logger.info("Running mcp-run-python via %s on port %d...", mode, http_port)
59+
if mode in ('streamable_http', 'streamable_http_stateless'):
60+
logger.info('Running mcp-run-python via %s on port %d...', mode, http_port)
6161
else:
6262
logger.info("Running mcp-run-python via %s...", mode)
6363

@@ -212,7 +212,7 @@ def _deno_run_args(
212212
]
213213
if dependencies is not None:
214214
args.append(f'--deps={",".join(dependencies)}')
215-
if mode == "streamable_http":
215+
if mode in ('streamable_http', 'streamable_http_stateless'):
216216
if http_port is not None:
217217
args.append(f"--port={http_port}")
218218
if http_host is not None:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "uv_build"
55
[project]
66
name = "mcp-run-python"
77
description = "Model Context Protocol server to run Python code in a sandbox."
8-
version = "0.0.21"
8+
version = "0.0.22"
99
authors = [{ name = "Samuel Colvin", email = "samuel@pydantic.dev" }]
1010
license = "MIT"
1111
readme = "README.md"

tests/test_mcp_servers.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
pytestmark = pytest.mark.anyio
2323

2424

25-
@pytest.fixture(name='run_mcp_session', params=['stdio', 'streamable_http'])
25+
@pytest.fixture(name='run_mcp_session', params=['stdio', 'streamable_http', 'streamable_http_stateless'])
2626
def fixture_run_mcp_session(
2727
request: pytest.FixtureRequest,
2828
) -> Callable[[list[str]], AbstractAsyncContextManager[ClientSession]]:
@@ -35,9 +35,9 @@ async def run_mcp(deps: list[str]) -> AsyncIterator[ClientSession]:
3535
async with ClientSession(read, write) as session:
3636
yield session
3737
else:
38-
assert request.param == 'streamable_http', request.param
38+
assert request.param in ('streamable_http', 'streamable_http_stateless'), request.param
3939
port = 3101
40-
async with async_prepare_deno_env('streamable_http', http_port=port, dependencies=deps) as env:
40+
async with async_prepare_deno_env(request.param, http_port=port, dependencies=deps) as env:
4141
p = subprocess.Popen(['deno', *env.args], cwd=env.cwd)
4242
try:
4343
url = f'http://localhost:{port}/mcp'

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)