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 fastmcp-pvl-corepip install fastmcp-pvl-coreThis 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
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
fastmcp-server-template— copier template that scaffolds new FastMCP servers on top of this library.- Active consumers:
markdown-vault-mcp,scholar-mcp,image-generation-mcp. - Public API changes here propagate to consumers via periodic
copier updateruns against the template. - See the template's README for the update flow and the expected project shape.
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 withMY_APP_BEARER_DEFAULT_SUBJECT=<value>). -
Mapped tokens —
MY_APP_BEARER_TOKENS_FILE=/path/to/tokens.tomlloads 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:
- Token present: prefer
claims["sub"](OIDC's standard subject claim); fall back toclient_idifsubis absent. The auth builders normaliseclient_idper mode:bearer-single→bearer_default_subject(default"bearer-anon").bearer-mapped→ the per-token subject from the TOML map.- OIDC modes (
oidc-proxy,remote) → typicallyclaims["sub"]wins (a real OIDC token always carriessub); theclient_idfallback is defensive. multi→ bearer-validated requests follow the bearer path, OIDC-validated requests follow the OIDC path.
- No token,
auth_mode == "none": returns the literal"local". - 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_aclfails fast withConfigurationErroron 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 to0is a silent no-op. Non-numeric or out-of-1..65535values log aWARNINGand 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 aWARNINGand 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.0and 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_PORTin 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.