Skip to content

Commit f551715

Browse files
committed
Evil hack to allow for runtime specification of resource mimetype
1 parent ee9367b commit f551715

1 file changed

Lines changed: 133 additions & 23 deletions

File tree

src/pdf2sqlite/mcp_server/resources.py

Lines changed: 133 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
import base64
44
import io
55
from dataclasses import dataclass
6+
from types import MethodType
7+
from typing import Any, Awaitable, Callable, Mapping, cast
68

79
from mcp.server.fastmcp import Context, FastMCP
10+
from mcp.server.fastmcp.resources.types import FunctionResource
811
from mcp.server.fastmcp.utilities.types import Image
912
from mcp.types import BlobResourceContents, EmbeddedResource
13+
from pydantic import AnyUrl
1014
from pypdf import PdfReader, PdfWriter
1115

1216
from .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+
88130
def 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

Comments
 (0)