33import base64
44import io
55from dataclasses import dataclass
6+ from types import MethodType
7+ from typing import Any , Awaitable , Callable , Mapping , cast
68
79from mcp .server .fastmcp import Context , FastMCP
10+ from mcp .server .fastmcp .resources .types import FunctionResource
811from mcp .server .fastmcp .utilities .types import Image
912from mcp .types import BlobResourceContents , EmbeddedResource
13+ from pydantic import AnyUrl
1014from pypdf import PdfReader , PdfWriter
1115
1216from .config import ServerConfig
@@ -71,7 +75,7 @@ async def make_embedded_pdf(self, uri: str, data: bytes) -> EmbeddedResource:
7175 return EmbeddedResource (
7276 type = "resource" ,
7377 resource = BlobResourceContents (
74- uri = uri ,
78+ uri = cast ( AnyUrl , uri ) ,
7579 mimeType = "application/pdf" ,
7680 blob = encoded ,
7781 _meta = {"size" : len (data )},
@@ -85,6 +89,44 @@ def as_image(self, data: bytes, mime_type: str | None) -> Image:
8589 return Image (data = data , format = subtype )
8690
8791
92+ def _patch_dynamic_blob_template (
93+ server : FastMCP ,
94+ uri_template : str ,
95+ * ,
96+ default_mime : str ,
97+ loader : Callable [
98+ [Mapping [str , object ], Context | None ],
99+ Awaitable [tuple [bytes , str | None ]],
100+ ],
101+ ) -> None :
102+ template = server ._resource_manager ._templates .get (uri_template )
103+ if template is None :
104+ raise RuntimeError (
105+ f"Resource template { uri_template } is not registered"
106+ )
107+
108+ async def create_resource (
109+ self : Any ,
110+ uri : str ,
111+ params : Mapping [str , object ],
112+ context : Context | None = None ,
113+ ) -> FunctionResource :
114+ payload , mime_type = await loader (params , context )
115+ return FunctionResource (
116+ uri = uri , # type: ignore[arg-type]
117+ name = self .name ,
118+ title = self .title ,
119+ description = self .description ,
120+ mime_type = mime_type or default_mime ,
121+ icons = self .icons ,
122+ annotations = self .annotations ,
123+ fn = lambda payload = payload : payload ,
124+ )
125+
126+ template .mime_type = default_mime
127+ object .__setattr__ (template , "create_resource" , MethodType (create_resource , template ))
128+
129+
88130def register_resources (server : FastMCP , service : ResourceService ) -> None :
89131 @server .resource (
90132 "pdf2sqlite://pdf/{pdf_id}" ,
@@ -127,59 +169,127 @@ async def read_figure(figure_id: int, ctx: Context | None = None) -> bytes: # n
127169 async def read_table_image (table_id : int , ctx : Context | None = None ) -> bytes : # noqa: ARG001
128170 return await service .load_table_image_blob (TableImageResource (table_id ))
129171
172+ async def _figure_loader (
173+ params : Mapping [str , object ],
174+ ctx : Context | None ,
175+ ) -> tuple [bytes , str | None ]: # noqa: ARG001
176+ figure_id = _require_int (params .get ("figure_id" ), "figure.figure_id" )
177+ return await service .load_figure_blob (FigureResource (figure_id ))
178+
179+ async def _table_loader (
180+ params : Mapping [str , object ],
181+ ctx : Context | None ,
182+ ) -> tuple [bytes , str | None ]: # noqa: ARG001
183+ table_id = _require_int (params .get ("table_id" ), "table.table_id" )
184+ data = await service .load_table_image_blob (
185+ TableImageResource (table_id )
186+ )
187+ return data , "image/jpeg"
130188
131- def build_page_payload (page : dict [str , int | str | None ]) -> dict [str , object ]:
132- pdf_id = int (page ["pdf_id" ])
133- page_number = int (page ["page_number" ])
189+ _patch_dynamic_blob_template (
190+ server ,
191+ "pdf2sqlite://figure/{figure_id}" ,
192+ default_mime = "application/octet-stream" ,
193+ loader = _figure_loader ,
194+ )
195+ _patch_dynamic_blob_template (
196+ server ,
197+ "pdf2sqlite://table-image/{table_id}" ,
198+ default_mime = "image/jpeg" ,
199+ loader = _table_loader ,
200+ )
201+
202+
203+ def _require_int (value : object | None , label : str ) -> int :
204+ if isinstance (value , bool ):
205+ return int (value )
206+ if isinstance (value , int ):
207+ return value
208+ if isinstance (value , str ):
209+ stripped = value .strip ()
210+ if not stripped :
211+ raise ValueError (f"{ label } must not be empty" )
212+ try :
213+ return int (stripped )
214+ except ValueError as exc :
215+ msg = f"{ label } must be an integer-compatible value, got { value !r} "
216+ raise ValueError (msg ) from exc
217+ if value is None :
218+ raise ValueError (f"{ label } is required" )
219+ raise TypeError (f"{ label } must be int-like, got { type (value ).__name__ } " )
220+
221+
222+ def _optional_int (value : object | None , label : str ) -> int | None :
223+ if value is None :
224+ return None
225+ return _require_int (value , label )
226+
227+
228+ def build_page_payload (page : Mapping [str , object ]) -> dict [str , object ]:
229+ pdf_id = _require_int (page .get ("pdf_id" ), "page.pdf_id" )
230+ page_number = _require_int (page .get ("page_number" ), "page.page_number" )
134231 resource_uri = build_pdf_page_uri (pdf_id , page_number )
135232 return {
136- "page_id" : int (page [ "id" ] ),
233+ "page_id" : _require_int (page . get ( "id" ), "page.id" ),
137234 "pdf_id" : pdf_id ,
138235 "page_number" : page_number ,
139236 "gist" : page .get ("gist" ),
140- "text_length" : page .get ("text_length" ),
141- "data_bytes" : page .get ("data_bytes" ),
237+ "text_length" : _optional_int ( page .get ("text_length" ), "page. text_length" ),
238+ "data_bytes" : _optional_int ( page .get ("data_bytes" ), "page. data_bytes" ),
142239 "resource" : resource_uri ,
143240 }
144241
145242
146- def build_pdf_payload (pdf : dict [str , object ]) -> dict [str , object ]:
147- pdf_id = int (pdf [ "id" ] )
243+ def build_pdf_payload (pdf : Mapping [str , object ]) -> dict [str , object ]:
244+ pdf_id = _require_int (pdf . get ( "id" ), "pdf.id" )
148245 return {
149246 "pdf_id" : pdf_id ,
150247 "title" : pdf .get ("title" ),
151248 "description" : pdf .get ("description" ),
152- "page_count" : pdf .get ("page_count" ),
249+ "page_count" : _optional_int ( pdf .get ("page_count" ), "pdf. page_count" ),
153250 "resource" : build_pdf_uri (pdf_id ),
154251 }
155252
156253
157- def build_figure_payload (figure : dict [str , object ]) -> dict [str , object ]:
158- figure_id = int (figure [ "id" ] )
254+ def build_figure_payload (figure : Mapping [str , object ]) -> dict [str , object ]:
255+ figure_id = _require_int (figure . get ( "id" ), "figure.id" )
159256 return {
160257 "figure_id" : figure_id ,
161258 "description" : figure .get ("description" ),
162259 "mime_type" : figure .get ("mime_type" ),
163- "data_bytes" : figure .get ("data_bytes" ),
260+ "data_bytes" : _optional_int ( figure .get ("data_bytes" ), "figure. data_bytes" ),
164261 "resource" : build_figure_uri (figure_id ),
165262 }
166263
167264
168- def build_table_payload (table : dict [str , object ]) -> dict [str , object ]:
169- table_id = int (table [ "id" ] )
265+ def build_table_payload (table : Mapping [str , object ]) -> dict [str , object ]:
266+ table_id = _require_int (table . get ( "id" ), "table.id" )
170267 payload : dict [str , object ] = {
171268 "table_id" : table_id ,
172269 "description" : table .get ("description" ),
173270 "caption_above" : table .get ("caption_above" ),
174271 "caption_below" : table .get ("caption_below" ),
175- "data_bytes " : table .get ("data_bytes " ),
272+ "text " : table .get ("text " ),
176273 "resource" : build_table_image_uri (table_id ),
177274 }
178- if "pdf_id" in table and "page_number" in table :
179- payload ["pdf_id" ] = table .get ("pdf_id" )
180- payload ["page_number" ] = table .get ("page_number" )
181- payload ["page_resource" ] = build_pdf_page_uri (
182- int (table ["pdf_id" ]),
183- int (table ["page_number" ]),
184- )
275+ text_length = _optional_int (table .get ("text_length" ), "table.text_length" )
276+ if text_length is not None :
277+ payload ["text_length" ] = text_length
278+ data_bytes = _optional_int (table .get ("data_bytes" ), "table.data_bytes" )
279+ if data_bytes is not None :
280+ payload ["data_bytes" ] = data_bytes
281+ xmin = _optional_int (table .get ("xmin" ), "table.xmin" )
282+ if xmin is not None :
283+ payload ["xmin" ] = xmin
284+ ymin = _optional_int (table .get ("ymin" ), "table.ymin" )
285+ if ymin is not None :
286+ payload ["ymin" ] = ymin
287+ pdf_id = _optional_int (table .get ("pdf_id" ), "table.pdf_id" )
288+ page_number = _optional_int (table .get ("page_number" ), "table.page_number" )
289+ if pdf_id is not None :
290+ payload ["pdf_id" ] = pdf_id
291+ if page_number is not None :
292+ payload ["page_number" ] = page_number
293+ if pdf_id is not None and page_number is not None :
294+ payload ["page_resource" ] = build_pdf_page_uri (pdf_id , page_number )
185295 return payload
0 commit comments