e904b667c6
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
279 lines
8.8 KiB
Python
279 lines
8.8 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import os
|
|
import sys
|
|
|
|
from fastmcp import FastMCP
|
|
|
|
from .inference import create_inference
|
|
from .selection import DEFAULT_MODEL, resolve_model
|
|
from .providers import InferenceProvider, provider_choices
|
|
from .tasks import create_task
|
|
|
|
|
|
def _parse_args() -> argparse.Namespace:
|
|
"""Parse command line arguments."""
|
|
parser = argparse.ArgumentParser(description="PaddleOCR MCP server.")
|
|
|
|
parser.add_argument(
|
|
"--model",
|
|
default=os.getenv("PADDLEOCR_MCP_MODEL", DEFAULT_MODEL),
|
|
help="Model to run. Env: PADDLEOCR_MCP_MODEL.",
|
|
)
|
|
parser.add_argument(
|
|
"--ppocr_source",
|
|
choices=provider_choices(),
|
|
default=os.getenv("PADDLEOCR_MCP_PPOCR_SOURCE", "local"),
|
|
help="Inference provider. Env: PADDLEOCR_MCP_PPOCR_SOURCE.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--http",
|
|
action="store_true",
|
|
help="Use Streamable HTTP instead of stdio.",
|
|
)
|
|
parser.add_argument(
|
|
"--host",
|
|
default="127.0.0.1",
|
|
help="HTTP bind host (with --http). Default: 127.0.0.1.",
|
|
)
|
|
parser.add_argument(
|
|
"--port",
|
|
type=int,
|
|
default=8000,
|
|
help="HTTP bind port (with --http). Default: 8000.",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
help="Enable verbose logging.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--pipeline_config",
|
|
default=os.getenv("PADDLEOCR_MCP_PIPELINE_CONFIG"),
|
|
help="Pipeline config file path (local). Env: PADDLEOCR_MCP_PIPELINE_CONFIG.",
|
|
)
|
|
parser.add_argument(
|
|
"--device",
|
|
default=os.getenv("PADDLEOCR_MCP_DEVICE"),
|
|
help="Inference device (local). Env: PADDLEOCR_MCP_DEVICE.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--aistudio-base-url",
|
|
dest="aistudio_base_url",
|
|
default=os.getenv("PADDLEOCR_MCP_AISTUDIO_BASE_URL"),
|
|
help="AI Studio API base URL (aistudio). Env: PADDLEOCR_MCP_AISTUDIO_BASE_URL.",
|
|
)
|
|
parser.add_argument(
|
|
"--qianfan-base-url",
|
|
dest="qianfan_base_url",
|
|
default=os.getenv("PADDLEOCR_MCP_QIANFAN_BASE_URL")
|
|
or "https://qianfan.baidubce.com/v2/ocr",
|
|
help="Qianfan API base URL (qianfan). Env: PADDLEOCR_MCP_QIANFAN_BASE_URL.",
|
|
)
|
|
parser.add_argument(
|
|
"--self-hosted-base-url",
|
|
dest="self_hosted_base_url",
|
|
default=os.getenv("PADDLEOCR_MCP_SELF_HOSTED_BASE_URL"),
|
|
help="Self-hosted service base URL (self_hosted). Env: PADDLEOCR_MCP_SELF_HOSTED_BASE_URL.",
|
|
)
|
|
parser.add_argument(
|
|
"--aistudio_access_token",
|
|
default=os.getenv("PADDLEOCR_MCP_AISTUDIO_ACCESS_TOKEN"),
|
|
help="AI Studio access token (aistudio). Env: PADDLEOCR_MCP_AISTUDIO_ACCESS_TOKEN.",
|
|
)
|
|
parser.add_argument(
|
|
"--qianfan_api_key",
|
|
default=os.getenv("PADDLEOCR_MCP_QIANFAN_API_KEY"),
|
|
help="Qianfan API key (qianfan). Env: PADDLEOCR_MCP_QIANFAN_API_KEY.",
|
|
)
|
|
parser.add_argument(
|
|
"--http-timeout",
|
|
dest="http_timeout",
|
|
type=int,
|
|
default=int(os.getenv("PADDLEOCR_MCP_HTTP_TIMEOUT", "600")),
|
|
help=(
|
|
"HTTP read timeout in seconds for synchronous APIs. "
|
|
"Env: PADDLEOCR_MCP_HTTP_TIMEOUT."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--aistudio-request-timeout",
|
|
dest="aistudio_request_timeout",
|
|
type=int,
|
|
default=int(os.getenv("PADDLEOCR_MCP_AISTUDIO_REQUEST_TIMEOUT", "120")),
|
|
help=(
|
|
"Per-request HTTP timeout in seconds for AI Studio API calls. "
|
|
"Env: PADDLEOCR_MCP_AISTUDIO_REQUEST_TIMEOUT."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--aistudio-poll-timeout",
|
|
dest="aistudio_poll_timeout",
|
|
type=int,
|
|
default=int(os.getenv("PADDLEOCR_MCP_AISTUDIO_POLL_TIMEOUT", "600")),
|
|
help=(
|
|
"Total job polling timeout in seconds for AI Studio. "
|
|
"Env: PADDLEOCR_MCP_AISTUDIO_POLL_TIMEOUT."
|
|
),
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def _validate_args(args: argparse.Namespace) -> None:
|
|
"""Validate command line arguments."""
|
|
if not args.http and (args.host != "127.0.0.1" or args.port != 8000):
|
|
print(
|
|
"Host and port arguments are only valid when using HTTP transport (see: `--http`).",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(2)
|
|
|
|
if args.ppocr_source == InferenceProvider.AISTUDIO.value:
|
|
if not args.aistudio_access_token:
|
|
print("Error: The AI Studio access token is required.", file=sys.stderr)
|
|
print(
|
|
"Please either set `--aistudio_access_token` or set the environment variable "
|
|
"`PADDLEOCR_MCP_AISTUDIO_ACCESS_TOKEN`.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(2)
|
|
elif args.ppocr_source == InferenceProvider.QIANFAN.value:
|
|
if not args.qianfan_api_key:
|
|
print("Error: The Qianfan API key is required.", file=sys.stderr)
|
|
print(
|
|
"Please either set `--qianfan_api_key` or set the environment variable "
|
|
"`PADDLEOCR_MCP_QIANFAN_API_KEY`.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(2)
|
|
elif args.ppocr_source == InferenceProvider.SELF_HOSTED.value:
|
|
if not args.self_hosted_base_url:
|
|
print(
|
|
"Error: The self-hosted service base URL is required.", file=sys.stderr
|
|
)
|
|
print(
|
|
f"Please set `--self-hosted-base-url` or the environment variable "
|
|
"`PADDLEOCR_MCP_SELF_HOSTED_BASE_URL`.",
|
|
file=sys.stderr,
|
|
)
|
|
sys.exit(2)
|
|
|
|
|
|
def _create_inference_from_args(args: argparse.Namespace, model: str):
|
|
provider = args.ppocr_source
|
|
|
|
if provider == InferenceProvider.LOCAL.value:
|
|
return create_inference(
|
|
model=model,
|
|
provider=provider,
|
|
config=args.pipeline_config,
|
|
device=args.device,
|
|
)
|
|
elif provider == InferenceProvider.AISTUDIO.value:
|
|
return create_inference(
|
|
model=model,
|
|
provider=provider,
|
|
token=args.aistudio_access_token,
|
|
base_url=args.aistudio_base_url,
|
|
request_timeout=float(args.aistudio_request_timeout),
|
|
poll_timeout=float(args.aistudio_poll_timeout),
|
|
)
|
|
elif provider == InferenceProvider.QIANFAN.value:
|
|
return create_inference(
|
|
model=model,
|
|
provider=provider,
|
|
base_url=args.qianfan_base_url,
|
|
api_key=args.qianfan_api_key,
|
|
http_timeout=args.http_timeout,
|
|
)
|
|
elif provider == InferenceProvider.SELF_HOSTED.value:
|
|
return create_inference(
|
|
model=model,
|
|
provider=provider,
|
|
base_url=args.self_hosted_base_url,
|
|
http_timeout=args.http_timeout,
|
|
)
|
|
else:
|
|
raise ValueError(f"Unknown provider: {provider}")
|
|
|
|
|
|
async def async_main() -> None:
|
|
"""Asynchronous main entry point."""
|
|
args = _parse_args()
|
|
_validate_args(args)
|
|
|
|
try:
|
|
model = resolve_model(args.model, args.ppocr_source)
|
|
except ValueError as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
inference = _create_inference_from_args(args, model)
|
|
|
|
try:
|
|
await inference.start()
|
|
|
|
task = create_task(model, inference)
|
|
|
|
server_name = f"PaddleOCR {model} MCP server"
|
|
mcp = FastMCP(
|
|
name=server_name,
|
|
mask_error_details=True,
|
|
)
|
|
|
|
task.register_tools(mcp)
|
|
|
|
log_level = "INFO" if args.verbose else "WARNING"
|
|
|
|
if args.http:
|
|
await mcp.run_async(
|
|
transport="streamable-http",
|
|
host=args.host,
|
|
port=args.port,
|
|
log_level=log_level,
|
|
)
|
|
else:
|
|
await mcp.run_async(log_level=log_level)
|
|
|
|
except Exception as e:
|
|
print(f"Failed to start the server: {e}", file=sys.stderr)
|
|
if args.verbose:
|
|
import traceback
|
|
|
|
traceback.print_exc(file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
finally:
|
|
await inference.stop()
|
|
|
|
|
|
def main() -> None:
|
|
"""Main entry point."""
|
|
asyncio.run(async_main())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|