deepresearch-flow 0.7.2__py3-none-any.whl → 0.7.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -908,7 +908,7 @@ def create_app(
908
908
  # Lazy import to avoid circular dependency
909
909
  from deepresearch_flow.paper.snapshot.mcp_server import (
910
910
  McpSnapshotConfig,
911
- create_mcp_app,
911
+ create_mcp_apps,
912
912
  resolve_static_export_dir,
913
913
  )
914
914
 
@@ -919,7 +919,7 @@ def create_app(
919
919
  limits=limits or ApiLimits(),
920
920
  origin_allowlist=cors_allowed_origins or ["*"],
921
921
  )
922
- mcp_app, mcp_lifespan = create_mcp_app(mcp_config)
922
+ mcp_apps, mcp_lifespan = create_mcp_apps(mcp_config)
923
923
 
924
924
  routes = [
925
925
  Route("/api/v1/config", _api_config, methods=["GET"]),
@@ -931,7 +931,8 @@ def create_app(
931
931
  Route("/api/v1/facets/{facet:str}/{facet_id:str}/stats", _api_facet_stats, methods=["GET"]),
932
932
  Route("/api/v1/facets/{facet:str}/by-value/{value:str}/papers", _api_facet_by_value_papers, methods=["GET"]),
933
933
  Route("/api/v1/facets/{facet:str}/by-value/{value:str}/stats", _api_facet_by_value_stats, methods=["GET"]),
934
- Mount("/mcp", app=mcp_app),
934
+ Mount("/mcp", app=mcp_apps["streamable-http"]),
935
+ Mount("/mcp-sse", app=mcp_apps["sse"]),
935
936
  ]
936
937
 
937
938
  # Pass MCP lifespan to ensure session manager initializes properly
@@ -5,7 +5,7 @@ import json
5
5
  import os
6
6
  from pathlib import Path
7
7
  import re
8
- from typing import Any
8
+ from typing import Any, Literal
9
9
 
10
10
  import httpx
11
11
  from starlette.applications import Starlette
@@ -76,14 +76,13 @@ class McpSnapshotConfig:
76
76
 
77
77
 
78
78
  class McpRequestGuardMiddleware(BaseHTTPMiddleware):
79
- def __init__(self, app, *, origin_allowlist: list[str]) -> None:
79
+ def __init__(self, app, *, origin_allowlist: list[str], allowed_methods: set[str] | None = None) -> None:
80
80
  super().__init__(app)
81
81
  self._allowlist = [origin.lower() for origin in origin_allowlist]
82
+ self._allowed_methods = {method.upper() for method in (allowed_methods or {"POST", "OPTIONS"})}
82
83
 
83
84
  async def dispatch(self, request: Request, call_next): # type: ignore[override]
84
- if request.method == "GET":
85
- return Response("Method Not Allowed", status_code=405)
86
- if request.method not in {"POST", "OPTIONS"}:
85
+ if request.method.upper() not in self._allowed_methods:
87
86
  return Response("Method Not Allowed", status_code=405)
88
87
  origin = request.headers.get("origin")
89
88
  if origin and not self._is_allowed_origin(origin):
@@ -108,23 +107,49 @@ def configure(config: McpSnapshotConfig) -> None:
108
107
  _CONFIG = config
109
108
 
110
109
 
111
- def create_mcp_app(config: McpSnapshotConfig) -> tuple[Starlette, Any]:
112
- """Create MCP app with middleware and return it along with lifespan.
113
-
114
- Returns:
115
- Tuple of (wrapped_app, lifespan_context) for use by parent Starlette.
116
- """
110
+ def _allowed_methods_for_transport(transport: Literal["streamable-http", "sse"]) -> set[str]:
111
+ if transport == "sse":
112
+ return {"GET", "POST", "OPTIONS"}
113
+ return {"POST", "OPTIONS"}
114
+
115
+
116
+ def create_mcp_transport_app(
117
+ config: McpSnapshotConfig,
118
+ *,
119
+ transport: Literal["streamable-http", "sse"] = "streamable-http",
120
+ ) -> tuple[Starlette, Any]:
121
+ """Create MCP app for a specific transport with transport-aware method guard."""
117
122
  configure(config)
118
- mcp_app = mcp.http_app(path="/", stateless_http=True)
123
+ mcp_app = mcp.http_app(path="/", transport=transport, stateless_http=(transport == "streamable-http"))
119
124
  wrapped = Starlette(
120
125
  routes=[Mount("/", app=mcp_app)],
121
126
  middleware=[
122
- Middleware(McpRequestGuardMiddleware, origin_allowlist=config.origin_allowlist),
127
+ Middleware(
128
+ McpRequestGuardMiddleware,
129
+ origin_allowlist=config.origin_allowlist,
130
+ allowed_methods=_allowed_methods_for_transport(transport),
131
+ ),
123
132
  ],
124
133
  )
125
134
  return wrapped, mcp_app.lifespan
126
135
 
127
136
 
137
+ def create_mcp_apps(config: McpSnapshotConfig) -> tuple[dict[str, Starlette], Any]:
138
+ """Create streamable-http and sse MCP apps.
139
+
140
+ Returns:
141
+ A tuple of (apps_by_transport, lifespan_context).
142
+ """
143
+ streamable_app, lifespan = create_mcp_transport_app(config, transport="streamable-http")
144
+ sse_app, _ = create_mcp_transport_app(config, transport="sse")
145
+ return {"streamable-http": streamable_app, "sse": sse_app}, lifespan
146
+
147
+
148
+ def create_mcp_app(config: McpSnapshotConfig) -> tuple[Starlette, Any]:
149
+ """Backward-compatible helper returning streamable-http MCP app."""
150
+ return create_mcp_transport_app(config, transport="streamable-http")
151
+
152
+
128
153
  def _get_config() -> McpSnapshotConfig:
129
154
  if _CONFIG is None:
130
155
  raise RuntimeError("MCP server not configured")
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+ import unittest
6
+
7
+ from deepresearch_flow.paper.snapshot.api import create_app
8
+ from deepresearch_flow.paper.snapshot.common import ApiLimits
9
+ from deepresearch_flow.paper.snapshot.mcp_server import (
10
+ McpSnapshotConfig,
11
+ _allowed_methods_for_transport,
12
+ )
13
+
14
+
15
+ class TestMcpTransport(unittest.TestCase):
16
+ @classmethod
17
+ def setUpClass(cls) -> None:
18
+ cls.tmpdir = tempfile.TemporaryDirectory()
19
+ cls.snapshot_db = Path(cls.tmpdir.name) / "snapshot.db"
20
+ cls.snapshot_db.touch()
21
+ cls.cfg = McpSnapshotConfig(
22
+ snapshot_db=cls.snapshot_db,
23
+ static_base_url="",
24
+ static_export_dir=None,
25
+ limits=ApiLimits(),
26
+ origin_allowlist=["*"],
27
+ )
28
+
29
+ @classmethod
30
+ def tearDownClass(cls) -> None:
31
+ cls.tmpdir.cleanup()
32
+
33
+ def test_streamable_transport_rejects_get(self) -> None:
34
+ self.assertNotIn("GET", _allowed_methods_for_transport("streamable-http"))
35
+
36
+ def test_sse_transport_allows_get(self) -> None:
37
+ self.assertIn("GET", _allowed_methods_for_transport("sse"))
38
+
39
+ def test_api_mounts_streamable_and_sse_endpoints(self) -> None:
40
+ app = create_app(
41
+ snapshot_db=self.snapshot_db,
42
+ static_base_url="",
43
+ cors_allowed_origins=["*"],
44
+ limits=ApiLimits(),
45
+ )
46
+ mount_paths = sorted(getattr(route, "path", "") for route in app.routes)
47
+ self.assertIn("/mcp", mount_paths)
48
+ self.assertIn("/mcp-sse", mount_paths)
49
+
50
+
51
+ if __name__ == "__main__":
52
+ unittest.main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepresearch-flow
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: Workflow tools for paper extraction, review, and research automation.
5
5
  Author-email: DengQi <dengqi935@gmail.com>
6
6
  License: MIT License
@@ -532,6 +532,22 @@ server {
532
532
  proxy_set_header X-Real-IP $remote_addr;
533
533
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
534
534
  }
535
+
536
+ # SSE transport for MCP clients that require Server-Sent Events
537
+ location ^~ /mcp-sse {
538
+ proxy_pass http://127.0.0.1:8001;
539
+ proxy_http_version 1.1;
540
+ proxy_set_header Connection "";
541
+ proxy_set_header Host $host;
542
+ proxy_set_header X-Real-IP $remote_addr;
543
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
544
+ proxy_buffering off;
545
+ proxy_cache off;
546
+ proxy_read_timeout 3600s;
547
+ proxy_send_timeout 3600s;
548
+ chunked_transfer_encoding off;
549
+ add_header X-Accel-Buffering no;
550
+ }
535
551
  }
536
552
 
537
553
  # Static assets (separate domain)
@@ -562,12 +578,15 @@ uv run deepresearch-flow paper db api serve \
562
578
  --host 0.0.0.0 --port 8001
563
579
  ```
564
580
 
565
- ### 3.1) MCP (FastMCP Streamable HTTP)
581
+ ### 3.1) MCP (FastMCP Streamable HTTP + SSE)
566
582
 
567
- This project exposes an MCP server mounted at `/mcp` on the snapshot API:
583
+ This project exposes MCP servers mounted on the snapshot API:
568
584
 
569
- - Endpoint: `http://<host>:8001/mcp` (same host/port as `paper db api serve`)
570
- - Transport: Streamable HTTP via `POST` only (no SSE; `GET` returns 405)
585
+ - Streamable HTTP endpoint: `http://<host>:8001/mcp`
586
+ - SSE endpoint: `http://<host>:8001/mcp-sse`
587
+ - Transport behavior:
588
+ - `/mcp`: Streamable HTTP via `POST` only (`GET` returns 405)
589
+ - `/mcp-sse`: SSE-enabled transport (supports `GET` handshake)
571
590
  - Protocol header: optional `mcp-protocol-version` (`2025-03-26` or `2025-06-18`)
572
591
  - Static reads: summary/source/translation are served as **text content** by reading snapshot static assets (local-first via `PAPER_DB_STATIC_EXPORT_DIR`, HTTP fallback via `PAPER_DB_STATIC_BASE` / `PAPER_DB_STATIC_BASE_URL`)
573
592
 
@@ -959,7 +978,7 @@ docker run --rm -p 8899:8899 \
959
978
  ```
960
979
 
961
980
  Notes:
962
- - nginx listens on 8899 and proxies `/api` and `/mcp` to the internal API at `127.0.0.1:8000`.
981
+ - nginx listens on 8899 and proxies `/api`, `/mcp`, and `/mcp-sse` to the internal API at `127.0.0.1:8000`.
963
982
  - Mount your snapshot DB to `/db/papers.db` inside the container.
964
983
  - Mount snapshot static assets to `/static` when serving assets from this container (default `PAPER_DB_STATIC_BASE` is `/static`).
965
984
  - If `PAPER_DB_STATIC_BASE` is a full URL (e.g. `https://static.example.com`), nginx still serves the frontend locally, while API responses use that external static base for asset links.
@@ -43,17 +43,18 @@ deepresearch_flow/paper/schemas/default_paper_schema.json,sha256=6h_2ayHolJj8JMn
43
43
  deepresearch_flow/paper/schemas/eight_questions_schema.json,sha256=VFKKpdZkgPdQkYIW5jyrZQ7c2TlQZwB4svVWfoiwxdg,1005
44
44
  deepresearch_flow/paper/schemas/three_pass_schema.json,sha256=8aNr4EdRiilxszIRBCC4hRNXrfIOcdnVW4Qhe6Fnh0o,689
45
45
  deepresearch_flow/paper/snapshot/__init__.py,sha256=1VLO36xxDB3J5Yoo-HH9vyI-4ev2HcivXN0sNLg8O5k,102
46
- deepresearch_flow/paper/snapshot/api.py,sha256=F_qehvCjxTBTGj9FmqP4NnJQayUPJm0N5e_8mm5JlDQ,37405
46
+ deepresearch_flow/paper/snapshot/api.py,sha256=z1TJmFeMKr5ZiNbZT1xTueVqUqQJD0WhBplDHKlFXRo,37476
47
47
  deepresearch_flow/paper/snapshot/builder.py,sha256=HbRcfNteMoP4RnQ4y2onZCm9XfnIvzXLn_EwsLZsDzY,38692
48
48
  deepresearch_flow/paper/snapshot/common.py,sha256=KAhlGlPgabOCe9Faps8BoDqin71qpkCfaL_ADCr_9vg,917
49
49
  deepresearch_flow/paper/snapshot/identity.py,sha256=k9x1EZPFBU1qgxzkTGvwVtDjLgcosmM_udPuvRLl0uI,7748
50
- deepresearch_flow/paper/snapshot/mcp_server.py,sha256=KGNCtOWiJ82wHQmrLNVhLwDugGtosqVvKWeLq4ZlBlg,23395
50
+ deepresearch_flow/paper/snapshot/mcp_server.py,sha256=c_WrM7PIMGRmvLg_3759NXc8wH5iwLCa6REBAnngwRg,24491
51
51
  deepresearch_flow/paper/snapshot/schema.py,sha256=DcVmAklLYyEeDoVV9jYw7hoMHnHd9Eziivl-LP2busY,8991
52
52
  deepresearch_flow/paper/snapshot/text.py,sha256=0RnxLowa6AdirdLsUYym6BhWbjwiP2Qj2oZeA-pjmdE,4368
53
53
  deepresearch_flow/paper/snapshot/unpacker.py,sha256=ScKSFdrQLJHrITHe9KAxgAEH-vAAnXLolvW9zeJ3wsc,8575
54
54
  deepresearch_flow/paper/snapshot/tests/__init__.py,sha256=G0IowrxHjGUIaqxcw6SvlcLFAtE5ZsleG6ECgd-sIdk,52
55
55
  deepresearch_flow/paper/snapshot/tests/test_identity.py,sha256=KDFixAUU9l68KOum7gf1IrD0Oy18dBCSXG7RbJTqflA,4520
56
56
  deepresearch_flow/paper/snapshot/tests/test_mcp_server_schema_compat.py,sha256=T7FtkKkGpZx5M7Z278F4iaQFfwS0_XXce_tRdTArt5k,7076
57
+ deepresearch_flow/paper/snapshot/tests/test_mcp_transport.py,sha256=Qh91te1XgzssTUfgCJUpq6Xjnw4tzhhr78bcI3Z4DpA,1622
57
58
  deepresearch_flow/paper/templates/__init__.py,sha256=p8W6kINvrf-T2X6Ow4GMr28syVOorFuMn0pbmieVzAw,35
58
59
  deepresearch_flow/paper/templates/deep_read.md.j2,sha256=vwVSPOzMBFIS72ez5XFBaKrDZGz0z32L3VGP6mNk434,4780
59
60
  deepresearch_flow/paper/templates/deep_read_phi.md.j2,sha256=6Yz2Kxk0czGDPkZiWX3b87glLYHwDU1afr6CkjS-dh8,1666
@@ -467,9 +468,9 @@ deepresearch_flow/translator/placeholder.py,sha256=mEgqA-dPdOsIhno0h_hzfpXpY2asb
467
468
  deepresearch_flow/translator/prompts.py,sha256=EvfBvBIpQXARDj4m87GAyFXJGL8EJeahj_rOmp9mv68,5556
468
469
  deepresearch_flow/translator/protector.py,sha256=yUMuS2FgVofK_MRXrcauLRiwNvdCCjNAnh6CcNd686o,11777
469
470
  deepresearch_flow/translator/segment.py,sha256=rBFMCLTrvm2GrPc_hNFymi-8Ih2DAtUQlZHCRE9nLaM,5146
470
- deepresearch_flow-0.7.2.dist-info/licenses/LICENSE,sha256=hT8F2Py1pe6flxq3Ufdm2UKFk0B8CBm0aAQfsLXfvjw,1063
471
- deepresearch_flow-0.7.2.dist-info/METADATA,sha256=2QzhwiS1G6q-XZyRXv7SGgzzsNueZ3PH5-q3g0jlP-Y,31331
472
- deepresearch_flow-0.7.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
473
- deepresearch_flow-0.7.2.dist-info/entry_points.txt,sha256=1uIKscs0YRMg_mFsg9NjsaTt4CvQqQ_-zGERUKhhL_Y,65
474
- deepresearch_flow-0.7.2.dist-info/top_level.txt,sha256=qBl4RvPJNJUbL8CFfMNWxY0HpQLx5RlF_ko-z_aKpm0,18
475
- deepresearch_flow-0.7.2.dist-info/RECORD,,
471
+ deepresearch_flow-0.7.3.dist-info/licenses/LICENSE,sha256=hT8F2Py1pe6flxq3Ufdm2UKFk0B8CBm0aAQfsLXfvjw,1063
472
+ deepresearch_flow-0.7.3.dist-info/METADATA,sha256=I8ERjmgnZ-IZ9WQvm6sPT_yKgozcL5g11sWgqh36Ti8,31955
473
+ deepresearch_flow-0.7.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
474
+ deepresearch_flow-0.7.3.dist-info/entry_points.txt,sha256=1uIKscs0YRMg_mFsg9NjsaTt4CvQqQ_-zGERUKhhL_Y,65
475
+ deepresearch_flow-0.7.3.dist-info/top_level.txt,sha256=qBl4RvPJNJUbL8CFfMNWxY0HpQLx5RlF_ko-z_aKpm0,18
476
+ deepresearch_flow-0.7.3.dist-info/RECORD,,