config-contract
Skill1 starsChanged 2 days agoMITView on GitHub
Use when adding a domain configuration field, env var, Dockerfile extension, mcpb install-screen entry, or release-manifest stamp: the sentinel-based config and customization contract.
- Reads credentials
Files
<!-- ===== TEMPLATE-OWNED — re-rendered on template updates. ===== -->Config Contract
Config & Customization Contract
Domain configuration composes fastmcp_pvl_core.ServerConfig inside your domain config class (see src/paperless_mcp/config.py). Add domain fields between the CONFIG-FIELDS-START / CONFIG-FIELDS-END sentinels, populate them in from_env between the CONFIG-FROM-ENV-START / CONFIG-FROM-ENV-END sentinels, and enforce their invariants in __post_init__ between the CONFIG-VALIDATE-START / CONFIG-VALIDATE-END sentinels. Validation belongs in __post_init__ rather than from_env because it then also covers a direct ProjectConfig(field=...); env_float / env_int bounds check only the env-sourced value, are inclusive-only, and cannot express cross-field rules. A variable that must be set is read with env(_ENV_PREFIX, "NAME", required=True) in from_env — it refuses at startup with a ConfigurationError and the generated docs mark it required — and returned from config_contract_env in tests/conftest.py. __post_init__ checks values and rejects them with ConfigurationError (already imported in config.py), never ValueError: serve reports only ConfigurationError as its one-line ERROR: configuration error: … exit. Never refuse an empty required field in __post_init__. The dataclass is frozen — read fields, don't assign (use object.__setattr__ if a field must be normalised). Never inherit from ServerConfig; always compose.
Env var prefix is PAPERLESS_MCP_ — all env reads go through fastmcp_pvl_core.env(_ENV_PREFIX, "SUFFIX", default) so naming stays consistent.
- Domain CLI subcommands go in the
# DOMAIN-COMMANDS-START/-ENDblock incli.py(likeCONFIG-FIELDSinconfig.py). Register them as@app.command()and use function-local imports for domain modules. The block is preserved acrosscopier update.
Config wizard
docs/javascripts/config-wizard/wizard-spec.json drives the guided-setup page. It is generated, produced by scripts/gen_config_surface.py on every copier copy/copier update and re-verified by scripts/gen_config_surface.py --check in CI — never hand-edit it. The runtime (wizard.js, generators.js, wizard-spec-schema.json, the generic tests) is template-owned and re-rendered the same way it always was.
To change what the wizard asks:
- A domain setting your project reads — give its
ProjectConfigfield (between theCONFIG-FIELDS-START/-ENDsentinels inconfig.py) ametadata={"help": ..., "tags": (...)}. The generator's AST scan discovers it from there; no wizard-spec edits needed. - A var the scan cannot see (a deprecated alias no longer read inside
ProjectConfig.from_env, or something read outside it entirely) — declare it inconfig-presentation.domain.ymlinstead. - A path the container needs mounted — add
dockerVolumeto the field'swizardhint:metadata={"help": ..., "tags": (...), "wizard": {"group": "Storage", "dockerVolume": "/data/corpus"}}. When the user supplies a host path, the wizard's Docker and Compose output emits a-v <that answer>:/data/corpusbind mount and sets the var to the container path, instead of an-enaming a host path the container cannot reach. Leaving the field blank emits neither — no mount and no override — so an optional path the user skipped never produces a command pointing at a file that was never mounted. UsedockerPathinstead for a fixed location on the state volume that the server creates itself: it substitutes the path when the var is already present and never adds a mount. The two are mutually exclusive and both must be absolute; the generator rejects a violation by name. - Coverage is enforced, not automatic — the generator fails loudly (
SystemExit, naming the var and its tags) if a collectedVar's tags match no env-file section, rather than silently dropping it from every generated file. Giving a domain field a tag no section lists is a config-presentation bug, and this catches it at generation time instead of shipping an undocumented var. There is, however, no orphan check: a stale or mistaken entry inconfig-presentation.domain.ymlthat nothing actually reads will generate into the wizard and env files anyway. Keep that file's contents matched to real reads yourself.
Generated docs tables
The same metadata drives the documentation, so a domain field is documented the moment it exists — never hand-write an env-var table:
docs/reference/configuration.mdis the complete generated reference: every collected var renders in exactly one section table (the generator'scomplete: trueguard fails generation if one would be missed). Domain fields render under## Domain variables, segmented into###sub-sections by each field'swizard: {group: ...}hint — the same grouping the config wizard shows — with ungrouped fields first. Hand-authored prose belongs around theGENERATED-ENV-TABLE-REF-*marker pairs (theDOMAIN-CONFIG-VARSblock holds this project's conceptual prose), never inside them.README.md's two config tables are curated subsets, not the full surface. Addreadmeto a field'stagsmetadata to feature it in the README's Domain configuration table; leave the rest to the reference. Keep the featured set small — it is a landing-page entry point.
mcpb install screen
packaging/mcpb/manifest.json.in's user_config and server.mcp_config.env objects are generated the same way (kind: mcpb-user-config): both derive from one curated fields: map, so a screen field and its env wiring cannot drift apart, and hand edits to those two objects are overwritten on the next generation — the rest of the manifest stays yours. The template baseline shows a deliberately minimal screen (server name, log level). Curate this project's screen in config-presentation.domain.yml under files: → packaging/mcpb/manifest.json.in → fields:: map an env var name to {id: ..., title: ..., type: string|boolean|number|directory|file, required: ..., default: ..., sensitive: ...} (everything but id falls back to the var's own metadata), or to null to drop a baseline field. A files: entry for a path the template does not declare is taken wholesale — that is how a project drives its own additional install-channel manifest from the same source of truth.
Dockerfile extension points
These sentinel blocks in Dockerfile are preserved across copier update. Add domain-specific apt packages, uv extras, state subdirs, and volume mounts inside them:
# DOCKERFILE-APT-DEPS-START/-END— extra apt packages installed into the runtime image# DOCKERFILE-UV-EXTRAS-START/-END—--extra <name>flags added to bothuv syncinvocations (deps cache layer + project install — adding only to one breaks the cache layer)# DOCKERFILE-STATE-DIRS-START/-END— state subdirectories created under/data(chowned to the runtime user)# DOCKERFILE-VOLUMES-START/-END—VOLUMEdeclarations on the final image
Dependency pins
A pin in [tool.uv] override-dependencies / constraint-dependencies is a workaround, never a policy: it exists because something upstream is broken, and it must go the moment that is fixed. Every entry therefore carries # until: <GitHub issue or PR URL> — on its own line, on a comment line directly above it, or on the array's opening line to cover every entry — naming the upstream bug, or a project issue tracking it. scripts/check_pins.py (run by ci.yml's lint job) fails the build when an entry has no such URL. Once that issue is closed, it raises a warning annotation on every run instead, so the reminder to lift the pin arrives on the next push without turning unrelated branches red. Keep one bound per package: a package bounded under [project] and in a [tool.uv] table fails the offline check, because Renovate cannot see the [tool.uv] tables and a raise on the [project] side would never move the lock. tests/test_dependency_pins.py runs the offline half locally.
Release manifest extension points
scripts/stamp_manifests.py runs inside every release PR (knope's prepare-release workflow invokes it with the computed version; pyproject.toml is knope's own versioned_files entry and the script never touches it). It rewrites uv.lock's self-version entry on every release, rc included, in the PEP 440 canonical spelling (-rc.N → rcN) — canonical because uv rewrites the entry to that spelling on any re-lock, and stamping it that way from the start means a mid-cycle re-lock changes nothing. The install-channel manifests — server.json and the Claude plugin pair — it stamps on stable versions only, so a stable tag never points at a manifest whose version lags it; on rc versions they stay at the last published stable — those manifests are the entries the MCP registry and the marketplace publish, and both surfaces are stable-only, so stamping an rc into them would advertise a candidate to everyone browsing the catalog (tests/test_release_flow_contract.py asserts all of this). That an rc's wheel is now on PyPI does not change this: reachability was never the whole reason, and the registry and marketplace entries are rolling pointers, not per-version ones. These sentinel blocks in that script are preserved across copier update. Add stamps for this project's own version-coupled manifests (another lockstep JSON/TOML) inside them:
# DOMAIN-MANIFESTS-HELPERS-START/-END— module-level helpers (use_load/_dumpfor JSON so the byte format matches whatscripts/gen_config_surface.pyasserts; raiseStampErrorfor a pin that cannot be stamped — never warn and continue)# DOMAIN-MANIFESTS-START/-END— the calls, insidemain(), whereversionis in scope; extendstampedwith every path you rewrite so it is staged into the release commit
The script must actually be invoked by knope's stamp Command, or a stale manifest ships silently; tests/test_release_flow_contract.py asserts that invocation coupling (the successor of the PSR-era assets pairing rule), so a stamp declared in one half only fails the gate rather than shipping a release commit with a stale file in it.
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 2
- pvliesdonk/markdown-vault-mcp★ 3799% same
- pvliesdonk/scholar-mcp★ 299% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository15 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- applying-template-updatesSkill
- authoring-issues-prsSkill
- designing-tool-outcomesSkill
- logging-standardSkill
- releasingSkill
- repository-protectionSkill
- researching-referencesSkill
- roadmappingSkill
- self-reviewingSkill
- tool-registrationSkill
- writing-documentationSkill
- writing-model-facing-textSkill
- writing-release-notesSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

