跳转至

OpenAPI 模块

应用工厂

toolregistry_server.openapi.create_openapi_app

create_openapi_app(route_table: RouteTable, title: str = 'ToolRegistry Server', version: str = '1.0.0', description: str = 'OpenAPI server for ToolRegistry tools', dependencies: Sequence[Any] | None = None, enable_etag: bool = True) -> FastAPI

Create a FastAPI application from a RouteTable.

Parameters:

Name Type Description Default
route_table RouteTable

The RouteTable to expose.

required
title str

API title for OpenAPI schema.

'ToolRegistry Server'
version str

API version for OpenAPI schema.

'1.0.0'
description str

API description for OpenAPI schema.

'OpenAPI server for ToolRegistry tools'
dependencies Sequence[Any] | None

Optional list of global dependencies (e.g., authentication).

None
enable_etag bool

Whether to enable ETag middleware for cache validation. Defaults to True.

True

Returns:

Type Description
FastAPI

A configured FastAPI application with:

FastAPI
  • Tool routes from the RouteTable
FastAPI
  • /tools endpoint for listing available tools
FastAPI
  • ETag middleware for cache validation (if enabled)
FastAPI
  • Dynamic OpenAPI schema that filters disabled tools

Raises:

Type Description
ImportError

If FastAPI is not installed.

Source code in src/toolregistry_server/openapi/__init__.py
def create_openapi_app(
    route_table: "RouteTable",
    title: str = "ToolRegistry Server",
    version: str = "1.0.0",
    description: str = "OpenAPI server for ToolRegistry tools",
    dependencies: "Sequence[Any] | None" = None,
    enable_etag: bool = True,
) -> "FastAPI":
    """Create a FastAPI application from a RouteTable.

    Args:
        route_table: The RouteTable to expose.
        title: API title for OpenAPI schema.
        version: API version for OpenAPI schema.
        description: API description for OpenAPI schema.
        dependencies: Optional list of global dependencies (e.g., authentication).
        enable_etag: Whether to enable ETag middleware for cache validation.
            Defaults to True.

    Returns:
        A configured FastAPI application with:
        - Tool routes from the RouteTable
        - /tools endpoint for listing available tools
        - ETag middleware for cache validation (if enabled)
        - Dynamic OpenAPI schema that filters disabled tools

    Raises:
        ImportError: If FastAPI is not installed.
    """
    try:
        from fastapi import FastAPI
    except ImportError as e:
        raise ImportError(
            "FastAPI is required for OpenAPI support. "
            "Install with: pip install toolregistry-server[openapi]"
        ) from e

    from .adapter import (
        add_tools_endpoint,
        route_table_to_router,
        setup_dynamic_openapi,
    )
    from .middleware import add_etag_middleware

    # Create app with optional global dependencies
    if dependencies:
        app = FastAPI(
            title=title,
            version=version,
            description=description,
            dependencies=list(dependencies),
        )
    else:
        app = FastAPI(
            title=title,
            version=version,
            description=description,
        )

    # Add ETag middleware for cache validation
    if enable_etag:
        add_etag_middleware(app, route_table)

    # Add /tools endpoint for listing available tools
    add_tools_endpoint(app, route_table)

    # Add routes from route table
    router = route_table_to_router(route_table)
    app.include_router(router)

    # Setup dynamic OpenAPI schema that filters disabled tools
    setup_dynamic_openapi(app, route_table)

    return app

适配器

toolregistry_server.openapi.adapter

Automatic route generation from a RouteTable.

Converts a :class:~toolregistry_server.RouteTable into a FastAPI :class:~fastapi.APIRouter by introspecting each registered route and dynamically creating Pydantic request models and route handlers.

add_tools_endpoint

add_tools_endpoint(app: FastAPI, route_table: RouteTable) -> None

Add a /tools endpoint that returns the list of available tools.

This endpoint supports ETag-based cache validation. The response includes the current ETag in both the response body and headers.

Parameters:

Name Type Description Default
app FastAPI

The FastAPI application instance.

required
route_table RouteTable

The RouteTable to query for tools.

required
Example
from fastapi import FastAPI
from toolregistry_server import RouteTable
from toolregistry_server.openapi.adapter import add_tools_endpoint

app = FastAPI()
route_table = RouteTable(registry)
add_tools_endpoint(app, route_table)
Source code in src/toolregistry_server/openapi/adapter.py
def add_tools_endpoint(app: "FastAPI", route_table: RouteTable) -> None:
    """Add a /tools endpoint that returns the list of available tools.

    This endpoint supports ETag-based cache validation. The response includes
    the current ETag in both the response body and headers.

    Args:
        app: The FastAPI application instance.
        route_table: The RouteTable to query for tools.

    Example:
        ```python
        from fastapi import FastAPI
        from toolregistry_server import RouteTable
        from toolregistry_server.openapi.adapter import add_tools_endpoint

        app = FastAPI()
        route_table = RouteTable(registry)
        add_tools_endpoint(app, route_table)
        ```
    """
    try:
        from fastapi import Request
        from fastapi.responses import JSONResponse
    except ImportError as e:
        raise ImportError(
            "FastAPI is required for OpenAPI support. "
            "Install with: pip install toolregistry-server[openapi]"
        ) from e

    @app.get("/tools", tags=["meta"])
    async def list_tools(request: Request):
        """List all available tools with their metadata.

        Returns a JSON object containing:
        - tools: List of tool information (name, namespace, method, path, description)
        - etag: Current ETag for cache validation

        Supports conditional requests via If-None-Match header.
        Returns 304 Not Modified if the ETag matches.
        """
        # Check If-None-Match header
        if_none_match = request.headers.get("If-None-Match")
        current_etag = route_table.etag

        if if_none_match and if_none_match == current_etag:
            return JSONResponse(
                content=None,
                status_code=304,
                headers={"ETag": current_etag},
            )

        # Build tools list — deferred tools are excluded from the initial
        # listing so that LLMs discover them via discover_tools.
        tools = []
        for route in route_table.list_routes(enabled_only=True, include_deferred=False):
            tools.append(
                {
                    "name": route.tool_name,
                    "namespace": route.namespace,
                    "method": route.method_name,
                    "path": route.path,
                    "description": route.description,
                }
            )

        return JSONResponse(
            content={"tools": tools, "etag": current_etag},
            headers={"ETag": current_etag},
        )

route_table_to_router

route_table_to_router(route_table: RouteTable, prefix: str = '') -> APIRouter

Convert a :class:~toolregistry_server.RouteTable into a FastAPI router.

Routes are generated for all registered tools regardless of their current enabled/disabled state. Each endpoint checks the route's enabled state at request time and returns HTTP 503 if the tool has been disabled, allowing runtime enable/disable without restarting the server.

Parameters:

Name Type Description Default
route_table RouteTable

The route table to convert.

required
prefix str

URL prefix for all generated routes.

''

Returns:

Name Type Description
A APIRouter

class:~fastapi.APIRouter with one POST route per registered tool.

Raises:

Type Description
ImportError

If FastAPI is not installed.

Source code in src/toolregistry_server/openapi/adapter.py
def route_table_to_router(
    route_table: RouteTable,
    prefix: str = "",
) -> "APIRouter":
    """Convert a :class:`~toolregistry_server.RouteTable` into a FastAPI router.

    Routes are generated for **all** registered tools regardless of their
    current enabled/disabled state. Each endpoint checks the route's enabled
    state at request time and returns HTTP 503 if the tool has been disabled,
    allowing runtime enable/disable without restarting the server.

    Args:
        route_table: The route table to convert.
        prefix: URL prefix for all generated routes.

    Returns:
        A :class:`~fastapi.APIRouter` with one POST route per registered tool.

    Raises:
        ImportError: If FastAPI is not installed.
    """
    try:
        from fastapi import APIRouter
    except ImportError as e:
        raise ImportError(
            "FastAPI is required for OpenAPI support. "
            "Install with: pip install toolregistry-server[openapi]"
        ) from e

    router = APIRouter(prefix=prefix)

    for route in route_table.list_routes(enabled_only=False):
        _add_route_from_entry(router, route, route_table)

    return router

setup_dynamic_openapi

setup_dynamic_openapi(app: FastAPI, route_table: RouteTable) -> None

Configure dynamic OpenAPI schema generation that filters out disabled tools.

This replaces FastAPI's default cached OpenAPI schema with a dynamic one that checks tool enable/disable status on every request to /openapi.json. Disabled tools are excluded from the schema so they do not appear in /docs or /openapi.json, and re-enabling them makes them visible again immediately.

Parameters:

Name Type Description Default
app FastAPI

The FastAPI application instance.

required
route_table RouteTable

The route table used for enable/disable status checks.

required
Source code in src/toolregistry_server/openapi/adapter.py
def setup_dynamic_openapi(app: "FastAPI", route_table: RouteTable) -> None:
    """Configure dynamic OpenAPI schema generation that filters out disabled tools.

    This replaces FastAPI's default cached OpenAPI schema with a dynamic one
    that checks tool enable/disable status on every request to ``/openapi.json``.
    Disabled tools are excluded from the schema so they do not appear in
    ``/docs`` or ``/openapi.json``, and re-enabling them makes them visible
    again immediately.

    Args:
        app: The FastAPI application instance.
        route_table: The route table used for enable/disable status checks.
    """
    try:
        from fastapi.openapi.utils import get_openapi
    except ImportError as e:
        raise ImportError(
            "FastAPI is required for OpenAPI support. "
            "Install with: pip install toolregistry-server[openapi]"
        ) from e

    def custom_openapi() -> dict[str, Any]:
        # Generate a fresh OpenAPI schema on every call (no caching)
        # so it always reflects the current enable/disable state.
        openapi_schema = get_openapi(
            title=app.title,
            version=app.version,
            description=app.description,
            routes=app.routes,
        )

        # Collect operation_ids of disabled tools
        disabled_operation_ids: set[str] = set()
        for route in route_table.list_routes(enabled_only=False):
            if not route.enabled:
                disabled_operation_ids.add(route.tool_name)

        # Filter out paths whose operations correspond to disabled tools
        if disabled_operation_ids and "paths" in openapi_schema:
            openapi_schema["paths"] = _filter_disabled_paths(
                openapi_schema["paths"], disabled_operation_ids
            )

        # Do NOT cache (app.openapi_schema is not set) so the schema
        # is regenerated on every request, reflecting runtime changes.
        return openapi_schema

    app.openapi = custom_openapi  # ty: ignore[invalid-assignment]

中间件

toolregistry_server.openapi.middleware

ETag middleware for OpenAPI server.

This module provides middleware for ETag-based cache validation, supporting conditional requests with If-None-Match headers.

ETagMiddleware

ETagMiddleware(app: ASGIApp, route_table: RouteTable)

Middleware for ETag-based cache validation.

This middleware intercepts requests and checks the If-None-Match header against the current ETag from the RouteTable. If they match, it returns a 304 Not Modified response. Otherwise, it adds the current ETag to the response headers.

The middleware only applies ETag handling to GET requests on specific paths (e.g., /tools, /openapi.json) to avoid interfering with tool execution endpoints.

Example
from fastapi import FastAPI
from toolregistry_server import RouteTable
from toolregistry_server.openapi.middleware import ETagMiddleware

app = FastAPI()
route_table = RouteTable(registry)
app.add_middleware(ETagMiddleware, route_table=route_table)

Attributes:

Name Type Description
app

The ASGI application to wrap.

route_table

The RouteTable instance for ETag generation.

etag_paths

Set of paths that should have ETag handling.

Initialize the ETag middleware.

Parameters:

Name Type Description Default
app ASGIApp

The ASGI application to wrap.

required
route_table RouteTable

The RouteTable instance for ETag generation.

required
Source code in src/toolregistry_server/openapi/middleware.py
def __init__(self, app: ASGIApp, route_table: RouteTable) -> None:
    """Initialize the ETag middleware.

    Args:
        app: The ASGI application to wrap.
        route_table: The RouteTable instance for ETag generation.
    """
    self.app = app
    self.route_table = route_table

__call__ async

__call__(scope: Scope, receive: Receive, send: Send) -> None

Process the request through the middleware.

Parameters:

Name Type Description Default
scope Scope

The ASGI scope dictionary.

required
receive Receive

The ASGI receive callable.

required
send Send

The ASGI send callable.

required
Source code in src/toolregistry_server/openapi/middleware.py
async def __call__(
    self,
    scope: Scope,
    receive: Receive,
    send: Send,
) -> None:
    """Process the request through the middleware.

    Args:
        scope: The ASGI scope dictionary.
        receive: The ASGI receive callable.
        send: The ASGI send callable.
    """
    if scope["type"] != "http":
        await self.app(scope, receive, send)
        return

    path = scope.get("path", "")
    method = scope.get("method", "")

    # Only apply ETag handling to GET requests on specific paths
    if method != "GET" or path not in self.ETAG_PATHS:
        await self.app(scope, receive, send)
        return

    # Extract If-None-Match header
    headers = dict(scope.get("headers", []))
    if_none_match = headers.get(b"if-none-match", b"").decode("utf-8")

    current_etag = self.route_table.etag

    # If ETag matches, return 304 Not Modified
    if if_none_match and if_none_match == current_etag:
        await send(
            {
                "type": "http.response.start",
                "status": 304,
                "headers": [
                    (b"etag", current_etag.encode("utf-8")),
                ],
            }
        )
        await send({"type": "http.response.body", "body": b""})
        return

    # Otherwise, process the request and add ETag to response
    async def send_with_etag(message: dict) -> None:
        if message["type"] == "http.response.start":
            headers = list(message.get("headers", []))
            # Add ETag header if not already present
            header_names = {h[0].lower() for h in headers}
            if b"etag" not in header_names:
                headers.append((b"etag", current_etag.encode("utf-8")))
            message = {**message, "headers": headers}
        await send(message)

    await self.app(scope, receive, send_with_etag)

add_etag_middleware

add_etag_middleware(app: FastAPI, route_table: RouteTable) -> None

Add ETag middleware to a FastAPI application.

This is a convenience function to add the ETagMiddleware to a FastAPI application with the correct configuration.

Parameters:

Name Type Description Default
app FastAPI

The FastAPI application instance.

required
route_table RouteTable

The RouteTable instance for ETag generation.

required
Example
from fastapi import FastAPI
from toolregistry_server import RouteTable
from toolregistry_server.openapi.middleware import add_etag_middleware

app = FastAPI()
route_table = RouteTable(registry)
add_etag_middleware(app, route_table)
Source code in src/toolregistry_server/openapi/middleware.py
def add_etag_middleware(app: FastAPI, route_table: RouteTable) -> None:
    """Add ETag middleware to a FastAPI application.

    This is a convenience function to add the ETagMiddleware to a FastAPI
    application with the correct configuration.

    Args:
        app: The FastAPI application instance.
        route_table: The RouteTable instance for ETag generation.

    Example:
        ```python
        from fastapi import FastAPI
        from toolregistry_server import RouteTable
        from toolregistry_server.openapi.middleware import add_etag_middleware

        app = FastAPI()
        route_table = RouteTable(registry)
        add_etag_middleware(app, route_table)
        ```
    """
    app.add_middleware(ETagMiddleware, route_table=route_table)  # ty: ignore[invalid-argument-type]