|
from mcp.server import Server |
|
from mcp.server.fastmcp import FastMCP, Context |
|
from mcp.server.sse import SseServerTransport |
|
from starlette.applications import Starlette |
|
from starlette.requests import Request |
|
from starlette.routing import Mount, Route |
|
from pdf2zh import translate_stream |
|
from pdf2zh.doclayout import ModelInstance |
|
from pathlib import Path |
|
|
|
import contextlib |
|
import io |
|
import os |
|
|
|
|
|
def create_mcp_app() -> FastMCP: |
|
mcp = FastMCP("pdf2zh") |
|
|
|
@mcp.tool() |
|
async def translate_pdf( |
|
file: str, lang_in: str, lang_out: str, ctx: Context |
|
) -> str: |
|
""" |
|
translate given pdf. Argument `file` is absolute path of input pdf, |
|
`lang_in` and `lang_out` is translate from and to language, and |
|
should be like google translate lang_code. `lang_in` can be `auto` |
|
if you can't determine input language. |
|
""" |
|
|
|
with open(file, "rb") as f: |
|
file_bytes = f.read() |
|
await ctx.log(level="info", message=f"start translate {file}") |
|
with contextlib.redirect_stdout(io.StringIO()): |
|
doc_mono_bytes, doc_dual_bytes = translate_stream( |
|
file_bytes, |
|
lang_in=lang_in, |
|
lang_out=lang_out, |
|
service="google", |
|
model=ModelInstance.value, |
|
thread=4, |
|
) |
|
await ctx.log(level="info", message="translate complete") |
|
output_path = Path(os.path.dirname(file)) |
|
filename = os.path.splitext(os.path.basename(file))[0] |
|
doc_mono = output_path / f"{filename}-mono.pdf" |
|
doc_dual = output_path / f"{filename}-dual.pdf" |
|
with open(doc_mono, "wb") as f: |
|
f.write(doc_mono_bytes) |
|
with open(doc_dual, "wb") as f: |
|
f.write(doc_dual_bytes) |
|
return f"""------------ |
|
translate complete |
|
mono pdf file: {doc_mono.absolute()} |
|
dual pdf file: {doc_dual.absolute()} |
|
""" |
|
|
|
return mcp |
|
|
|
|
|
def create_starlette_app(mcp_server: Server, *, debug: bool = False) -> Starlette: |
|
sse = SseServerTransport("/messages/") |
|
|
|
async def handle_sse(request: Request) -> None: |
|
async with sse.connect_sse(request.scope, request.receive, request._send) as ( |
|
read_stream, |
|
write_stream, |
|
): |
|
await mcp_server.run( |
|
read_stream, write_stream, mcp_server.create_initialization_options() |
|
) |
|
|
|
return Starlette( |
|
debug=debug, |
|
routes=[ |
|
Route("/sse", endpoint=handle_sse), |
|
Mount("/messages/", app=sse.handle_post_message), |
|
], |
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
import argparse |
|
|
|
mcp = create_mcp_app() |
|
mcp_server = mcp._mcp_server |
|
parser = argparse.ArgumentParser(description="Run MCP SSE-based PDF2ZH server") |
|
|
|
parser.add_argument( |
|
"--sse", |
|
default=False, |
|
action="store_true", |
|
help="Run the server with SSE transport or STDIO", |
|
) |
|
parser.add_argument( |
|
"--host", type=str, default="127.0.0.1", required=False, help="Host to bind" |
|
) |
|
parser.add_argument( |
|
"--port", type=int, default=3001, required=False, help="Port to bind" |
|
) |
|
|
|
args = parser.parse_args() |
|
if args.sse and args.host and args.port: |
|
import uvicorn |
|
|
|
starlette_app = create_starlette_app(mcp_server, debug=True) |
|
uvicorn.run(starlette_app, host=args.host, port=args.port) |
|
else: |
|
mcp.run() |
|
|