1. Conduid
  2. Security
  3. fastmcp-pvl-core
MCP server · Security

fastmcp-pvl-core

Shared FastMCP infrastructure: auth, middleware, logging, server-factory helpers

Unclaimed security
37Low

Scored 5 months ago · breakdown

About fastmcp-pvl-core

fastmcp-pvl-core is an MCP server in the Security category: shared FastMCP infrastructure: auth, middleware, logging, server-factory helpers.

Install

uvx
uvx fastmcp-pvl-core
pip
pip install fastmcp-pvl-core

This server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.

Ask AI

✦

Ask AI about fastmcp-pvl-core

Powered by Claude · Grounded in docs

I know everything about fastmcp-pvl-core. Ask me about installation, configuration, usage, or troubleshooting.

Security checks

  • ✓README presentThe repository ships documentation.

5 checks have not been run yet.

README

fastmcp-pvl-core

Shared FastMCP infrastructure for the pvliesdonk/*-mcp server family: auth, middleware, logging, config helpers, server-factory building blocks.

Ecosystem

API stability

This package is stable at 2.x and follows semantic versioning: breaking changes bump the major version, new features bump the minor, bugfixes bump the patch. "Public API" means symbols re-exported from the top-level fastmcp_pvl_core package (see __all__), which intentionally covers both the runtime surface (auth, middleware, factory builders, env/config helpers) and the CLI parser helpers consumed by downstream server.py entrypoints. Modules prefixed with _ are internal and may change without a major-version bump.

Install

uv add fastmcp-pvl-core
# If you use RemoteAuthProvider mode:
uv add "fastmcp-pvl-core[remote-auth]"
# For attaching a remote Python debugger inside a container image:
uv add "fastmcp-pvl-core[debug]"

Usage

See src/fastmcp_pvl_core/ for the full surface. Typical usage:

from fastmcp import FastMCP
from fastmcp_pvl_core import (
    ServerConfig, build_auth, build_instructions,
    wire_middleware_stack, env,
)

config = ServerConfig.from_env("MY_APP")
mcp = FastMCP(
    name="my-app",
    instructions=build_instructions(read_only=False, env_prefix="MY_APP", domain_line="…"),
    auth=build_auth(config),
)
wire_middleware_stack(mcp)

Per-user subject mapping (bearer auth)

Bearer auth has two modes:

  • Single token — MY_APP_BEARER_TOKEN=<token> accepts one shared token. Authenticated callers all share the same subject (default "bearer-anon"; override with MY_APP_BEARER_DEFAULT_SUBJECT=<value>).

  • Mapped tokens — MY_APP_BEARER_TOKENS_FILE=/path/to/tokens.toml loads a token→subject map at startup. Each token resolves to a distinct subject string for downstream attribution (audit logs, ACLs, request metadata).

# tokens.toml
[tokens]
"ghp_alice_xxxxxxxx" = "user:alice@example.com"
"sk_ci_yyyyyyyy"     = "service:ci-bot"

If both MY_APP_BEARER_TOKEN and MY_APP_BEARER_TOKENS_FILE are set, the file wins and a WARNING is logged. Subject strings are opaque to the library; the <kind>:<id> convention (user:, service:, token:) is documentation only.

If MY_APP_BEARER_TOKENS_FILE is set but the file is missing, unparseable, or schema-invalid, the loader raises fastmcp_pvl_core.ConfigurationError at startup — the server fails fast rather than silently denying every request. The exception type is part of the public API; downstream code can import and except it as a stable contract.

MY_APP_BEARER_DEFAULT_SUBJECT only applies when bearer auth runs in single-token mode (either standalone or as the bearer side of multi mode alongside OIDC). It is ignored when MY_APP_BEARER_TOKENS_FILE is set, including in multi mode — mapped mode uses the per-token subjects from the TOML file.

Identifying the caller — get_subject

Tools, middleware, and resource handlers can call fastmcp_pvl_core.get_subject() to retrieve the subject of the current request without knowing which auth mode is active:

from fastmcp_pvl_core import get_subject

@mcp.tool
def whoami() -> str:
    subject = get_subject()
    return subject or "anonymous"

Resolution order:

  1. Token present: prefer claims["sub"] (OIDC's standard subject claim); fall back to client_id if sub is absent. The auth builders normalise client_id per mode:
    • bearer-single → bearer_default_subject (default "bearer-anon").
    • bearer-mapped → the per-token subject from the TOML map.
    • OIDC modes (oidc-proxy, remote) → typically claims["sub"] wins (a real OIDC token always carries sub); the client_id fallback is defensive.
    • multi → bearer-validated requests follow the bearer path, OIDC-validated requests follow the OIDC path.
  2. No token, auth_mode == "none": returns the literal "local".
  3. No token, auth required: returns None — caller decides whether to fall back or error.

Authorization (opt-in) — AuthorizationMiddleware

Tools, resources, and prompts can opt into per-subject access control by setting meta={"required_scope": "<scope>"} at registration. A configured AuthorizationMiddleware enforces the static check and filters list_* responses to what the caller can use:

from pathlib import Path
from fastmcp_pvl_core import (
    AuthorizationMiddleware, load_acl, make_acl_authorizer, check_authorization,
)

authorizer = make_acl_authorizer(load_acl(Path("/etc/my-app/acl.toml")))
mcp.add_middleware(AuthorizationMiddleware(authorizer=authorizer))

@mcp.tool(meta={"required_scope": "write"})
async def edit_document(project_id: str, doc_id: str, body: str) -> None:
    # Coarse "write" gate already passed at middleware. Per-project gate here:
    check_authorization(f"write:{project_id}")
    ...

ACL TOML schema (loaded by load_acl):

[subjects]
"user:alice@example.com" = ["read", "write"]
"user:admin@example.com" = ["*"]              # wildcard scope
"service:ci-bot"         = ["read"]
"local"                  = ["*"]              # stdio mode

Key properties:

  • Opt-in per component. Tools / resources / prompts without meta["required_scope"] are unrestricted regardless of caller.
  • * is the only library-treated special scope ("any required scope passes"). All other scopes are opaque strings; downstream chooses the vocabulary.
  • Subject-side wildcards (* as an ACL key) are rejected at load time.
  • load_acl fails fast with ConfigurationError on every malformed condition — never silent denial.
  • ACL is loaded once at startup. Restart to pick up changes.
  • Authorization scopes are application-level and distinct from the OAuth scopes carried in tokens.
  • Subject is logged on every deny at WARNING. The wire-side payload omits the subject by default to limit cross-user info disclosure; pass AuthorizationMiddleware(..., expose_subject_in_error=True) to include it (e.g. for internal-only servers).

For the full design rationale and deviations from the originating issue, see docs/specs/authorization-submodule.md.

Remote debugging in containers

Containerised consumers can opt into a remote Python debugger by calling maybe_start_debugpy(env_prefix) early in their CLI entrypoint, passing the same per-app prefix the server uses for the rest of its config:

from fastmcp_pvl_core import configure_logging_from_env, maybe_start_debugpy

def main() -> None:
    configure_logging_from_env()
    maybe_start_debugpy("MY_APP")  # no-op unless MY_APP_DEBUG_PORT is set
    ...

Environment contract ({PREFIX} matches the argument):

  • {PREFIX}_DEBUG_PORT — TCP port to listen on. Unset, blank, or any value that parses to 0 is a silent no-op. Non-numeric or out-of-1..65535 values log a WARNING and the helper returns without raising.
  • {PREFIX}_DEBUG_WAIT — when truthy (1/true/yes/on, case-insensitive), block startup until the IDE attaches. Default is non-blocking.
  • If debugpy.listen() itself fails (port in use, permission denied, debugpy-internal error), the helper logs a WARNING and continues — a debug-port problem must never crash the server.

Install the optional debug extra on images that need the listener:

uv add "fastmcp-pvl-core[debug]"   # quote brackets in zsh
# or, equivalently:
uv add debugpy

The helper logs a WARNING and continues if debugpy is unavailable, so it is safe to ship in default scaffolds.

⚠️ Security: the listener binds 0.0.0.0 and debugpy's DAP protocol is unauthenticated — any peer that can reach the port has arbitrary code execution as the server process. Only enable {PREFIX}_DEBUG_PORT in environments where the port is reachable solely from a trusted developer workstation, e.g. kubectl port-forward, docker run -p 127.0.0.1:5678:5678 (loopback bind), or an SSH tunnel. Never publish the debug port on a public network.

License

MIT

README copy captured when this server was indexed; it may lag the repository. The original is authoritative.

Questions

About fastmcp-pvl-core

How do I install fastmcp-pvl-core?

Run uvx fastmcp-pvl-core, then add the server to your MCP client's configuration.

Is fastmcp-pvl-core safe to use with an AI agent?

Its trust score is 37 out of 100 (low). It passes 1 of the 1 static security checks run so far. It has no ConduID identity yet, so agent calls to it are not receipted.

Is fastmcp-pvl-core still maintained?

Conduid hasn't recorded a commit date for this repository yet. Check the repository directly for recent activity.