From 4f3f548b1b55d4c7477250fc3373b3bd6d0d397f Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sat, 12 Sep 2026 20:40:07 +0900 Subject: streamable-httpトランスポート対応を追加 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP SDK 2.xのMCPServer.run(transport="streamable-http")を使い、stdioに 加えてStreamable HTTPトランスポートで起動できるようにする。SSEはMCP仕様で 非推奨のため選択肢から除外し、transport enumはstdioとstreamable-httpの 2種類のみとした。 transportの選択はTOML [server] tableで指定し、CLI引数は追加しない。 仕様の「CLI上書きを行わない」「TOML集約」原則に準拠する。[server]省略時は stdio(後方互換)、streamable-http時はhost=127.0.0.1 port=8000を既定値とする。 変更内容: - config.py: ServerConfig dataclass、VALID_TRANSPORTS定数、AppConfig.server フィールド、load_configの[server]読込・validation追加 - cli.py: main()でconfig.server.transportに応じたrun()引数分岐 - tests/conftest.py: app_config fixtureにserver=ServerConfig()追加 - tests/test_config.py: [server]の正常系・異常系・境界値テスト追加 (省略時stdio既定値、streamable-http読込、sse/http/ws/unknown拒否、 port境界値0/65536/-1/文字列、host空文字拒否) - doc/config.md: [server]セクション追記、設定項目一覧表更新 - config.example.toml: [server]コメント例追記 - doc/Specifications.md: スコープ表にMCPトランスポート行追加 - doc/Design.md: 責務表と起動フロー更新 - doc/mcp.md: 概要のtransport記載更新 - README.md: streamable-http起動例追記 - script/smoke_streamable_http.py: streamable-http経由の検証スクリプト追加 検証: - ruff format/check、pyright 0 errors、pytest 50 passed - 実GPU・実モデルでstreamable-http経由generate_imageの全ルート確認 (HTTP接続→MCP initialize→tools/list→tools/call→PNG生成・返却) - 生成結果: 335KB PNG、512x512、20 steps、CFG 7.0 --- tests/conftest.py | 2 ++ tests/test_config.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) (limited to 'tests') diff --git a/tests/conftest.py b/tests/conftest.py index d364dbd..862b4f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ from mcp_sd15.config import ( GenerationConfig, ModelConfig, OutputConfig, + ServerConfig, ) @@ -28,6 +29,7 @@ def app_config(tmp_path: Path) -> AppConfig: inpaint=ControlModelConfig(True, "inpaint.safetensors"), ), output=OutputConfig(tmp_path / "outputs", False), + server=ServerConfig(), ) diff --git a/tests/test_config.py b/tests/test_config.py index c24064b..9a6bf89 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -123,3 +123,75 @@ def test_can_skip_path_checks_for_schema_only_use(tmp_path: Path) -> None: config = load_config(path, validate_paths=False) assert config.model.root == (tmp_path / "missing").resolve() assert config.generation.scheduler == "dpmpp_2m_karras" + + +def test_server_defaults_to_stdio_when_omitted(tmp_path: Path) -> None: + path = tmp_path / "config.toml" + path.write_text( + f'[model]\nroot = "{tmp_path.as_posix()}"\ncheckpoint = "base.safetensors"\n', + encoding="utf-8", + ) + config = load_config(path, validate_paths=False) + assert config.server.transport == "stdio" + assert config.server.host == "127.0.0.1" + assert config.server.port == 8000 + + +def test_server_reads_streamable_http(tmp_path: Path) -> None: + path = tmp_path / "config.toml" + path.write_text( + f'[model]\nroot = "{tmp_path.as_posix()}"\ncheckpoint = "base.safetensors"\n' + '[server]\ntransport = "streamable-http"\nhost = "0.0.0.0"\nport = 9000\n', + encoding="utf-8", + ) + config = load_config(path, validate_paths=False) + assert config.server.transport == "streamable-http" + assert config.server.host == "0.0.0.0" + assert config.server.port == 9000 + + +@pytest.mark.parametrize( + "transport", + ["sse", "http", "ws", "unknown"], +) +def test_rejects_invalid_transport(tmp_path: Path, transport: str) -> None: + path = tmp_path / "config.toml" + path.write_text( + f'[model]\nroot = "{tmp_path.as_posix()}"\ncheckpoint = "base.safetensors"\n' + f'[server]\ntransport = "{transport}"\n', + encoding="utf-8", + ) + with pytest.raises(ConfigError, match="transport"): + load_config(path, validate_paths=False) + + +@pytest.mark.parametrize( + "port, message", + [ + (0, "port"), + (65536, "port"), + (-1, "port"), + ("8000", "port"), + ], +) +def test_rejects_invalid_port(tmp_path: Path, port: str | int, message: str) -> None: + path = tmp_path / "config.toml" + port_literal = f'"{port}"' if isinstance(port, str) else str(port) + path.write_text( + f'[model]\nroot = "{tmp_path.as_posix()}"\ncheckpoint = "base.safetensors"\n' + f'[server]\ntransport = "streamable-http"\nport = {port_literal}\n', + encoding="utf-8", + ) + with pytest.raises(ConfigError, match=message): + load_config(path, validate_paths=False) + + +def test_rejects_empty_host(tmp_path: Path) -> None: + path = tmp_path / "config.toml" + path.write_text( + f'[model]\nroot = "{tmp_path.as_posix()}"\ncheckpoint = "base.safetensors"\n' + '[server]\ntransport = "streamable-http"\nhost = ""\n', + encoding="utf-8", + ) + with pytest.raises(ConfigError, match="host"): + load_config(path, validate_paths=False) -- cgit v1.2.3