41 Commits
Author SHA1 Message Date
John Lancaster d13ecd6718 added the cli-client skill 2026-09-04 19:58:43 -05:00
John Lancaster 5cefca852d tabbed spa reference page 2026-09-04 08:24:37 -05:00
John Lancaster 78a489c690 fixed mcp routing 2026-09-03 23:46:47 -05:00
John Lancaster 9312784c2f added machine-readable references 2026-09-03 23:46:30 -05:00
John Lancaster 09d2a4bcaf using tab panels in the spa 2026-09-03 23:45:10 -05:00
John Lancaster 86c7d54244 tab spa example 2026-09-03 23:31:39 -05:00
John Lancaster f75b24705e consistency updates 2026-09-01 23:34:02 -05:00
John Lancaster b87b1df642 action slot 2026-09-01 23:32:14 -05:00
John Lancaster bf11b7865d table customization 2026-09-01 22:54:56 -05:00
John Lancaster be579c347e agent skills details in docs 2026-08-30 14:40:32 -05:00
John Lancaster 9eb4ccbc6e doc updates 2026-08-30 11:49:58 -05:00
John Lancaster bbaa84720c prompts and resources as tools 2026-08-30 11:42:26 -05:00
John Lancaster b2ac4102f7 nicegui component pattern 2026-08-30 10:52:40 -05:00
John Lancaster fd5ce6f63b edit dialog 2026-08-30 10:34:45 -05:00
John Lancaster 783ecf421e prune 2026-08-30 09:40:16 -05:00
John Lancaster f6752313be expanded other pages 2026-08-30 09:19:54 -05:00
John Lancaster 12f916455b nicegui styling 2026-08-30 01:22:03 -05:00
John Lancaster 65669a2100 nicegui table updates 2026-08-30 01:01:50 -05:00
John Lancaster 3e2fc0ef25 dataclasses enhancement 2026-08-30 00:22:18 -05:00
John Lancaster 88474a75f5 doc updates for new structure 2026-08-30 00:16:58 -05:00
John Lancaster afedcda930 nicegui component mechanics 2026-08-30 00:06:22 -05:00
John Lancaster f5b65ecf0a test changes 2026-08-29 22:28:55 -05:00
John Lancaster c31a78206f debug launch config 2026-08-29 21:06:02 -05:00
John Lancaster de95477480 big rework 2026-08-29 20:10:30 -05:00
John Lancaster 73eb490537 utils.read_file 2026-08-29 19:13:20 -05:00
John Lancaster c866d1bdb0 component mechanics 2026-08-29 17:01:09 -05:00
John Lancaster 512db5f526 started tables reference page 2026-08-27 21:54:36 -05:00
John Lancaster 0638fe2fd7 instructions update 2026-08-27 18:54:49 -05:00
John Lancaster 8a994ff47b test updates 2026-08-27 18:52:41 -05:00
John Lancaster f3bbbfc25f mcp updates 2026-08-27 18:48:21 -05:00
John Lancaster e999437b93 nicegui reference updates 2026-08-27 18:09:10 -05:00
John Lancaster f1dd6ab940 skill authoring 2026-08-27 17:20:16 -05:00
John Lancaster 3d21e9136c quasar helpers 2026-08-08 10:10:22 -05:00
John Lancaster 6ec12a100a search_skills 2026-08-08 00:14:31 -05:00
John Lancaster 9be7c27410 better vscode integration 2026-08-08 00:05:58 -05:00
John Lancaster a157489634 toml updates 2026-08-07 23:59:03 -05:00
John Lancaster b5d6e60d45 nicegui component 2026-08-07 23:48:08 -05:00
John Lancaster f240486a7e swapped docs symlink 2026-08-07 21:07:23 -05:00
John Lancaster 5005cd7001 reorg 2026-08-07 20:47:37 -05:00
John Lancaster 88ff4c2c71 prompt markdown 2026-08-07 20:30:54 -05:00
John Lancaster 5b6d5aaec4 migration 2026-08-07 20:07:21 -05:00
187 changed files with 6866 additions and 5970 deletions
+52
View File
@@ -0,0 +1,52 @@
# personal-mcp MCP Usage
This repository is resource-first.
- Canonical skill guidance lives in `src/personal_mcp/docs/skills/<skill-id>/SKILL.md`.
- Skills are exposed through FastMCP's native `skill://` resource family.
- Prompts are exposed through native MCP prompt operations (`prompts/list`, `prompts/get`).
- General documentation pages are exposed through `resource://docs/{path*}`.
- The server intentionally does not provide compatibility tool projections for resources or prompts.
When a task appears to match a documented implementation pattern in `personal-mcp`, use this sequence:
1. Prefer an already attached native skill resource.
2. Otherwise browse native MCP resources and compare available `skill://<name>/SKILL.md` descriptions.
3. Read the best matching main skill file, or at most 2 candidate main files.
4. Read `skill://<name>/_manifest` only when supporting material may be useful.
5. Fetch only the relevant supporting paths from that manifest.
6. Reconcile skill guidance with the actual repository code before proposing or making changes.
Preferred MCP resource order:
1. `skill://<name>/SKILL.md`
2. `skill://<name>/_manifest` when needed
3. `skill://<name>/<supporting-path>` for selected supporting files
Selection rules:
- Prefer the closest `name` and `description` match.
- Keep context bounded; do not load many skill documents speculatively.
- If confidence is low after reading at most two main files, ask one clarifying question before loading more context.
Repository-specific guidance:
- For tasks about adding or modifying a skill, use `skill://copilot-customization/SKILL.md` when relevant.
- Keep skills provider-native; do not add tool projections for resource or prompt access.
## Python Checks
After changes run these commands to confirm functionality. Resolve any errors
```python
uv run ruff check .
```
```python
uv run ty check
```
```python
uv run pytest
```
@@ -1,19 +1,19 @@
--- ---
name: Authoring Content name: Authoring Content
description: "Use when editing Markdown under docs/. Routes authors to the canonical docs ownership, layout, and symlink guidance." description: "Use when editing Markdown under src/personal_mcp/docs/. Routes authors to the canonical docs ownership and layout guidance."
applyTo: 'docs/**/*.md' applyTo: 'src/personal_mcp/docs/**/*.md'
--- ---
For edits under `docs/`, use the [Authoring Guide](../../docs/authoring.md) as the entry point for content placement and contracts. For edits under `src/personal_mcp/docs/`, use the [Authoring Guide](../../src/personal_mcp/docs/authoring.md) as the entry point for content placement and contracts.
For source-tree ownership, symlink, packaging, or runtime questions, follow [Source Tree Ownership](../../docs/authoring.md). Treat that section as authoritative instead of restating its guidance here. For source-tree ownership, packaging, or runtime questions, follow [Source Tree Ownership](../../src/personal_mcp/docs/authoring.md). Treat that section as authoritative instead of restating its guidance here.
Primary references: Primary references:
- [Skill contract](../../docs/contracts/skill_contract.md) - [Skill contract](../../src/personal_mcp/docs/contracts/skill_contract.md)
- [Prompt contract](../../docs/contracts/prompt.md) - [Prompt contract](../../src/personal_mcp/docs/contracts/prompt.md)
- [Frontmatter contract](../../docs/contracts/frontmatter.md) - [Frontmatter contract](../../src/personal_mcp/docs/contracts/frontmatter.md)
- [URI contract](../../docs/contracts/uris.md) - [URI contract](../../src/personal_mcp/docs/contracts/uris.md)
- `skill://zensical-docs/SKILL.md` - `skill://zensical-docs/SKILL.md`
Inspect `skill://zensical-docs/_manifest` only when a supporting documentation reference is needed. Inspect `skill://zensical-docs/_manifest` only when a supporting documentation reference is needed.
+1 -1
View File
@@ -10,6 +10,6 @@ For FastMCP implementation or protocol questions, load `skill://mcp-details/SKIL
Inspect `skill://mcp-details/_manifest` only when a source reference is needed, then read the relevant supporting path. For FastMCP Python APIs, prefer the supporting reference that covers SDKs and FastMCP. Inspect `skill://mcp-details/_manifest` only when a source reference is needed, then read the relevant supporting path. For FastMCP Python APIs, prefer the supporting reference that covers SDKs and FastMCP.
This repository is resource-first and exposes resources as tools only as a fallback. In tool-only clients, use `list_resources` and `read_resource` with the same `skill://` URIs. This repository exposes skills through native MCP resources and prompts through native MCP prompt operations.
Reconcile the skill guidance with the installed FastMCP version and the repository's existing implementation before editing. Reconcile the skill guidance with the installed FastMCP version and the repository's existing implementation before editing.
@@ -1,11 +1,16 @@
--- ---
name: Pytest Scaffolding Guidance name: Pytest Scaffolding Guidance
description: Route tests edits to the Personal MCP pytesting resource. description: Use when working under tests/. Route test edits to pytesting guidance and never create or expand tests unless the user explicitly asks.
applyTo: 'tests/**' applyTo: 'tests/**'
--- ---
When editing files under `tests/`, use `skill://pytesting/SKILL.md` as the primary guidance source for test scaffolding and pytest authoring decisions. When editing files under `tests/`, use `skill://pytesting/SKILL.md` as the primary guidance source for test scaffolding and pytest authoring decisions.
Hard rule:
- Do not create new test files, test cases, or test scaffolding unless the user explicitly asks for tests in the current request.
- If tests could help but were not requested, mention them as an optional next step instead of adding them.
Execution pattern: Execution pattern:
1. Load `skill://pytesting/SKILL.md` first. 1. Load `skill://pytesting/SKILL.md` first.
@@ -16,4 +21,4 @@ Execution pattern:
If task intent is ambiguous, ask one clarifying question before editing. If task intent is ambiguous, ask one clarifying question before editing.
Be sure to also refer to the [testing page](../../docs/testing.md) page for design detail Be sure to also refer to the [testing page](../../src/personal_mcp/docs/testing.md) for design detail.
@@ -0,0 +1,17 @@
---
name: Skill Authoring
description: "Use when creating or editing skills under src/personal_mcp/docs/skills/. Enforces the Agent Skills specification and FastMCP skill provider compatibility."
applyTo: 'src/personal_mcp/docs/skills/**'
---
# Skill Authoring
Before editing files under `src/personal_mcp/docs/skills/`, consult all of these references:
- [Agent Skills quickstart](https://agentskills.io/skill-creation/quickstart)
- [Agent Skills best practices](https://agentskills.io/skill-creation/best-practices)
- [FastMCP skill providers](https://gofastmcp.com/servers/providers/skills)
Treat each immediate subdirectory of `src/personal_mcp/docs/skills/` as an independent Agent Skill. Keep its structure and content compliant with the Agent Skills pattern and compatible with the FastMCP skill provider.
Reconcile the external guidance with the repository's existing skill conventions and contracts before making changes.
@@ -1,46 +0,0 @@
## Plan: Docs-First FastMCP End State
Create a docs-first FastMCP architecture where all Markdown remains in docs/ as the only source of truth, each skill is Anthropic-compatible in its own directory, skill metadata lives in SKILL.md frontmatter, and packaged docs are served through importlib.resources so stdio deployments work from installed wheels.
**Steps**
1. Phase 1: Define the end-state content contract. Confirm canonical structure as docs/skills/<skill-id>/SKILL.md plus docs/skills/<skill-id>/references/..., with strict per-skill ownership and no metadata.yaml sidecar. Also define stable skill-id rules (kebab-case, immutable after release). Deliverable: update the current docs/ directory with the finalized end-state content contract from this step.
2. Phase 1: Define SKILL.md frontmatter schema with Pydantic-compatible fields: id, version, name, description, tags, capabilities, depends_on, and references manifest entries. The references manifest must map logical reference ids to relative paths so each skill can reorganize references internally without changing global server code. Depends on step 1. Deliverable: update the current docs/ directory with the finalized SKILL.md frontmatter schema from this step.
3. Phase 1: Define URI contract with explicit break-and-replace policy. Recommend resource://catalog/skills_index, resource://catalog/skills/{skill_id}, resource://skills/{skill_id}/document, resource://skills/{skill_id}/references/{ref_id}, and resource://docs/{path*}. Evolving URIs and reference ids requires direct replacement, with no aliases or compatibility shims. Depends on steps 1-2. Deliverable: update the current docs/ directory with the finalized URI contract and break-and-replace policy from this step.
4. Phase 2: Build a docs registry loader that reads packaged docs via importlib.resources.files(...) Traversable APIs, parses SKILL.md frontmatter, validates schema, and creates an in-memory registry keyed by skill_id. Fail fast for duplicate ids, missing files, broken reference mappings, or invalid depends_on. Depends on steps 2-3.
5. Phase 2: Register FastMCP resources from the registry using RFC6570 templates (including wildcard paths where appropriate), read-only/idempotent annotations, explicit mime types, and on_duplicate_resources="error" for startup safety. Depends on step 4.
6. Phase 2: Add discovery surfaces as resources first, then tool fallback. Keep catalog discovery in resources, then add ResourcesAsTools for tool-only clients. Add thin discovery tools only for parity and optional BM25/regex tool search when catalog/tool volume grows enough to affect token efficiency. Define canonical fallback tool names (`list_resources`, `read_resource`, `search_patterns`, `get_pattern_by_id`, `get_skill_document_by_id`), research host-specific naming behavior for GitHub Copilot, Cursor, Claude Desktop, and generic MCP clients, and require client-side name mapping or intentionally documented aliases when providers expose namespaced wrappers. Depends on step 5.
7. Phase 3: Implement packaging so docs/ is copied into package resource space at build time (wheel + sdist) while docs/ remains canonical in source control. Use importlib.resources at runtime only; avoid direct filesystem assumptions. Depends on steps 4-6.
8. Phase 3: Remove materialization coupling between skill source modules and docs. The website build reads docs/ directly, while MCP reads packaged docs resources from the installed package. This preserves one authored source with two distribution surfaces. Depends on step 7.
9. Phase 4: Add validation and CI gates: frontmatter schema checks, URI uniqueness checks, reference integrity checks, docs build check, package content check, and stdio smoke checks that read representative skill/document resources from an installed wheel. Depends on steps 5-8.
10. Phase 4: Add long-term maintainability guardrails: architecture decision record for URI and schema contracts, skill authoring checklist, and release checklist for evolving references safely within one skill. Parallel with step 9 after core architecture is stable.
**Relevant files**
- /home/john/Documents/prompts/docs/index.md — Keep top-level docs entry and explain docs-first architecture contract.
- /home/john/Documents/prompts/docs/skills — Canonical location for all skill content, including SKILL.md and references.
- /home/john/Documents/prompts/pyproject.toml — Build inclusion rules for packaged markdown resources in wheel/sdist.
- /home/john/Documents/prompts/src/personal_mcp/main.py — App/server startup wiring for resource registry initialization.
- /home/john/Documents/prompts/src/personal_mcp/mcp.py — FastMCP instance composition and transform registration.
- /home/john/Documents/prompts/src/personal_mcp/catalog/server.py — Catalog resource and fallback discovery behavior.
- /home/john/Documents/prompts/src/personal_mcp/skills/document_loader.py — Replace file-path assumptions with importlib.resources docs registry loading.
- /home/john/Documents/prompts/src/personal_mcp/web/materialize_skill_docs.py — De-scope or retire materialization once docs-first runtime is authoritative.
**Verification**
1. Run uv run zensical build to verify docs/ remains valid and site output is stable.
2. Run uv run pytest -q with tests that validate frontmatter parsing, URI generation, reference mapping, and catalog responses.
3. Run a packaging integrity check using importlib.resources.files(...) to confirm packaged docs resources exist and are readable from an installed wheel.
4. Run a stdio MCP smoke test that lists resources and reads at least one skill document and one reference document.
5. Run fallback-client smoke tests verifying list_resources/read_resource tools work and return expected metadata for both static and templated resources, and that GitHub Copilot, Cursor, Claude Desktop, and protocol-level SDK tests use canonical tool names or documented mapped aliases.
**Decisions**
- Anthropic compatibility: strict skill directory pattern with SKILL.md and references subtree.
- Metadata strategy: YAML frontmatter in SKILL.md (no separate metadata file).
- Discovery strategy: resource-first catalog with tool fallback for tool-only MCP clients.
- Included scope: ideal end-state architecture, contracts, validation, and packaging for stdio operation.
- Excluded scope: migration mechanics from current implementation, backward-compat shim details, and docs visual redesign.
**Further Considerations**
1. Prefer recursive references support under each skill plus frontmatter manifest ids, so skill teams can reorganize internal reference folders without URI churn.
2. Define a hard rule that skill_id and directory name must match exactly to eliminate namespace/slug drift classes of bugs.
3. Do not provide URI aliases; client updates must track canonical URI contract changes directly.
-169
View File
@@ -1,169 +0,0 @@
**Phase 3 Results: Packaging Contract and Surface Decoupling (Wheel/sdist Resources + Docs-Only Authoring)**
This section finalizes Phase 3 by defining how authored docs are packaged as runtime resources, how runtime loading avoids filesystem assumptions, and how website and MCP distribution surfaces are decoupled while sharing one authored source.
### Greenfield Framing (Normative)
This Phase 3 design assumes a full refactor with intentional break-and-replace behavior:
1. No compatibility shims, aliases, adapter layers, or dual-read runtime paths.
2. No runtime dependency on repository checkout layout.
3. Runtime docs access is package-resource-only.
4. Canonical authoring remains in `docs/` in source control.
### Research Baseline (Packaging + Runtime)
Authoritative references used for this phase:
1. Python `importlib.resources` docs (`files`, `Traversable`, and zip-safe behavior)
2. Python packaging guidance for wheel/sdist data inclusion
3. Hatchling build target configuration guidance for including non-code files
4. Existing repository constraints from Steps 4-5 (registry-first, deterministic startup, resource-first discovery)
Best-practice conclusions applied to this design:
1. Package docs as build artifacts so runtime reads work from installed wheels.
2. Keep docs source-of-truth in one place (`docs/`) and project into package resource space at build time.
3. Avoid `Path(__file__)`/repo-root probing in runtime paths.
4. Enforce parity across wheel and sdist so local/dev/prod behavior does not drift.
### Phase 3 Responsibilities (Normative)
Phase 3 MUST:
1. Ensure authored markdown under `docs/` is included in wheel and sdist artifacts.
2. Ensure runtime docs registry/document reads use `importlib.resources` only.
3. Ensure MCP runtime behavior is independent of current working directory or checkout structure.
4. Ensure website docs build continues to consume source `docs/` directly.
5. Remove materialization/path-probing coupling from runtime loader code.
6. Preserve deterministic packaged docs layout for registry/resource URI generation.
### Packaging Contract (Wheel + sdist)
Canonical packaging behavior:
1. Source-authored docs remain at repository root: `docs/`.
2. Build projects docs into package resource space under `personal_mcp/docs/` inside artifacts.
3. Runtime anchor for docs loading is `importlib.resources.files("personal_mcp").joinpath("docs")`.
4. Build artifacts MUST include:
- top-level docs pages used by discovery/overview
- `docs/skills/<skill-id>/SKILL.md`
- `docs/skills/<skill-id>/references/**`
Parity requirements:
1. Wheel and sdist contain equivalent docs content for runtime use.
2. Missing docs resources in either artifact is a hard validation failure.
### Build-System Plan (pyproject + build)
Primary target file:
1. `pyproject.toml`
Configuration goals:
1. Add explicit build inclusion rules so docs resources are shipped in wheel artifacts.
2. Add explicit sdist inclusion rules so docs are present for source builds.
3. Keep inclusion deterministic and auditable (no implicit glob side effects beyond intended docs content).
4. Ensure packaged destination path matches runtime anchor (`personal_mcp/docs`).
Implementation note:
1. Use Hatchling-native inclusion mapping (for example force-include or equivalent target-level include mapping) to project `docs/` into package resource space.
2. Prefer one clear packaging path over multiple fallback packaging mechanisms.
### Runtime Loader Contract (No Filesystem Assumptions)
Primary target file:
1. `src/personal_mcp/skills/document_loader.py`
Required runtime behavior:
1. Remove repository-root discovery helpers and path-probing candidates.
2. Remove metadata-based document path overrides that bypass canonical skill layout.
3. Resolve SKILL and reference documents via package-resource-relative paths only.
4. Keep reads UTF-8 and deterministic.
5. Raise explicit errors for missing packaged resources; no fallback probing.
Prohibited runtime behavior:
1. No `Path(__file__).resolve().parents[...]` lookup for docs.
2. No implicit fallback to source-tree `docs/` during runtime reads.
3. No slug-guessing or namespace substitution for path recovery.
### Surface Decoupling Contract (Website vs MCP)
Website surface:
1. Website build pipeline consumes source `docs/` directly (`uv run zensical build`).
2. Static output (`site/`) remains a build artifact served by web mounting logic.
MCP surface:
1. MCP runtime serves docs from packaged resources loaded by registry/resource handlers.
2. MCP does not read `site/` and does not depend on website build artifacts.
Decoupling guarantees:
1. One authored source (`docs/`), two distribution surfaces (website + MCP runtime).
2. Changes to website serving do not alter MCP resource loading semantics.
3. Changes to MCP runtime loader do not require website materialization logic.
### Integration Plan for Existing Modules
Primary integration targets:
1. `pyproject.toml`: add wheel/sdist docs inclusion mapping.
2. `src/personal_mcp/skills/document_loader.py`: replace filesystem probing with package-resource loading.
3. `src/personal_mcp/main.py`: keep startup composition deterministic once registry/resource registration is in place.
4. `src/personal_mcp/mcp.py`: maintain registry-driven resource composition as canonical runtime surface.
5. `src/personal_mcp/web/docs_mount.py`: continue static-site mount behavior without coupling to MCP runtime docs loading.
Cleanup targets:
1. Remove obsolete references to materialization-only modules if no longer present/used.
2. Remove dead code paths that attempt source-tree fallback loading.
### Validation and Test Plan (Phase 3 Scope)
Build/package validation:
1. Build wheel and sdist in CI/local.
2. Inspect artifacts to confirm `personal_mcp/docs/**` exists and includes representative skill/reference files.
3. Install built wheel in isolated environment and verify resource reads via `importlib.resources.files(...)`.
Runtime validation:
1. Run MCP in an environment where repo-root docs paths are unavailable and confirm reads still succeed.
2. Verify representative URIs resolve (skill document and reference document).
3. Confirm startup fails clearly if required packaged docs resources are missing.
Decoupling validation:
1. Run `uv run zensical build` to verify website pipeline still consumes source `docs/`.
2. Confirm MCP runtime does not require `site/` presence.
3. Confirm web static serving behavior is unchanged when docs are built.
Expected command path in this repo:
1. `uv run pytest -q`
2. `uv run zensical build`
### Acceptance Criteria for Phase 3 Completion
Phase 3 is complete when all are true:
1. Wheel and sdist include docs resources in deterministic package paths.
2. Runtime docs loading works from installed artifacts using `importlib.resources` only.
3. Runtime docs loading has no checkout-path dependency and no fallback probing.
4. Website docs build remains source-docs-driven and independent of MCP runtime loading.
5. No compatibility shims, aliases, or dual runtime loader paths exist.
### Non-goals for Phase 3
1. No Step 6 discovery-tool fallback implementation details.
2. No URI aliasing or backward-compat transition mechanics.
3. No redesign of skill frontmatter/schema contracts already finalized in earlier steps.
4. No web UI visual redesign or docs IA overhaul.
-84
View File
@@ -1,84 +0,0 @@
**Step 1 Results: End-State Content Contract**
This section finalizes Step 1 by defining the canonical authored content model.
### Step Deliverable
- Update the current `docs/` directory with the finalized Step 1 content contract from this document.
### Canonical source of truth
- All authored Markdown lives under `docs/`.
- MCP resources and static docs are two distribution surfaces of the same authored files.
- No parallel authored markdown is allowed in `src/` or other package-only paths.
### Canonical skill shape (Anthropic-compatible)
Each skill is one directory under `docs/skills/`:
```text
docs/
skills/
<skill-id>/
SKILL.md
references/
... (one or more markdown files, optional nested folders)
```
Rules:
- `SKILL.md` is required for every skill.
- `references/` is the only place for skill-specific supporting docs.
- Nested folders inside `references/` are allowed so a skill can reorganize internals without changing global architecture.
- Skill directories are independent ownership boundaries; no cross-skill file writes.
### File placement and ownership boundaries
- Top-level project docs stay in `docs/*.md`.
- Skill docs stay in `docs/skills/<skill-id>/...`.
- A skill may link to other skills, but must not store content inside another skill's directory.
- Server/runtime code may index and serve docs, but must not be the source of authored markdown.
### Metadata location constraint
- Skill metadata is embedded in YAML frontmatter in `SKILL.md`.
- No `metadata.yaml` sidecar in the end state.
- Reference lookup metadata (ids to relative paths) is declared from `SKILL.md` frontmatter, not inferred as a hidden global convention.
### Skill-id contract (change-friendly)
`skill-id` is the public identifier and SHOULD satisfy all rules below:
- Format: lowercase kebab-case only.
- Character set: `a-z`, `0-9`, and `-`.
- Must start with a letter.
- No underscores, spaces, dots, or uppercase characters.
- Directory name should equal `skill-id` in each committed revision.
- Frontmatter `id` should equal directory name in each committed revision.
- Treat `skill-id` as immutable after release; any rename is a breaking replacement and clients must move to the new id.
Example valid ids:
- `fastapi-uv-docker`
- `zensical-docs`
- `pytesting`
Example invalid ids:
- `fastapi_uv_docker` (underscore)
- `Zensical-Docs` (uppercase)
- `docs.zensical` (dot)
### Invariants this contract guarantees
- One authored source tree (`docs/`) for both website and MCP.
- One skill directory maps to one skill identity per revision.
- Namespace/slug drift is minimized by keeping directory and frontmatter ids aligned per revision.
- Per-skill reference structure can evolve without changing cross-skill architecture.
- Packaging for stdio is deterministic because authored content is path-stable.
### Non-goals for Step 1
- No URI versioning policy details yet (handled in Step 3).
- No full frontmatter schema details yet (handled in Step 2).
- No migration instructions from current architecture (out of scope for this plan).
-298
View File
@@ -1,298 +0,0 @@
**Step 2 Results: SKILL.md Frontmatter and FastMCP Metadata Contract**
This section finalizes Step 2 by defining the canonical SKILL.md frontmatter schema, separating Anthropic-supported fields from repository extension fields, and mapping frontmatter to FastMCP-native metadata surfaces for resources and tools.
### Step Deliverable
- Update the current `docs/` directory with the finalized Step 2 frontmatter and metadata contract content from this document.
### Anthropic Frontmatter Support (Research Baseline)
Across Anthropic API and Agent Skills specification surfaces:
- Required for custom skill bundles: `name`, `description`.
- `name` constraints (Agent Skills API docs): 1-64 chars, lowercase letters/numbers/hyphens, no XML tags, and must not use reserved words `anthropic` or `claude`.
- `description` constraints (Agent Skills API docs): 1-1024 chars, non-empty, no XML tags.
Portable optional fields from the Agent Skills specification:
- `license`
- `compatibility`
- `metadata`
- `allowed-tools` (experimental)
Claude Code-specific optional fields (supported by Claude Code skills docs):
- `when_to_use`, `argument-hint`, `arguments`
- `disable-model-invocation`, `user-invocable`
- `allowed-tools`, `disallowed-tools`
- `model`, `effort`, `context`, `agent`, `hooks`, `paths`, `shell`
Contract decision for this repository:
- Treat `name` and `description` as required in all SKILL.md files, even where a client could infer defaults.
- Keep Anthropic-facing semantics in standard fields and keep MCP indexing metadata in a namespaced extension block.
- Preserve forward compatibility by allowing additive optional metadata fields over time.
### Canonical Frontmatter Schema For This Repository
Use this exact two-layer pattern:
1. Anthropic layer (portable): top-level fields intended for Anthropic/Agent Skills behavior.
2. Repository layer (runtime indexing): one namespaced block, `x-personal-mcp`, for MCP catalog and routing metadata.
Canonical shape:
```yaml
---
name: <skill-id>
description: <what this skill does and when to use it>
# Optional Anthropic/Agent Skills fields (use only when needed)
when_to_use: <extra trigger guidance>
allowed-tools: <space-separated string or YAML list>
disable-model-invocation: false
user-invocable: true
license: <optional>
compatibility: <optional>
# Repository-specific metadata (authoritative for MCP indexing)
x-personal-mcp:
id: <skill-id>
version: <semver>
tags:
- <tag>
capabilities:
- resource://skills/<skill-id>/document
depends_on: []
references:
<ref-id>:
path: references/<file>.md
mime_type: text/markdown
title: <short title>
---
```
### Repository Metadata Field Rules (`x-personal-mcp`)
- `id` required: must follow Step 1 skill-id rules and equal directory name.
- `version` required: semantic version string.
- `tags` optional: list of kebab-case discovery labels.
- `capabilities` required: list of MCP URIs this skill publishes.
- `depends_on` optional: list of other skill ids.
- `references` optional map:
- key is `ref-id` (kebab-case).
- `path` is a skill-relative markdown path and must stay inside the same skill directory.
- nested folders under `references/` are allowed.
- `mime_type` defaults to `text/markdown` if omitted.
- `title` is an optional display label.
- renaming `ref-id` values is allowed when needed; optional aliases may be used during transitions.
### Pydantic Models For Frontmatter Validation
Define the Step 2 contract with Pydantic v2 models and change-friendly validation.
Normative model sketch:
```python
from __future__ import annotations
import re
from pathlib import PurePosixPath
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
SKILL_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$")
SEMVER_RE = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$")
class ReferenceEntry(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
path: str
mime_type: str = "text/markdown"
title: str | None = None
@field_validator("path")
@classmethod
def validate_reference_path(cls, value: str) -> str:
p = PurePosixPath(value)
if p.is_absolute() or ".." in p.parts:
raise ValueError("reference path must be a relative in-skill path")
if not str(p).startswith("references/"):
raise ValueError("reference path must stay under references/")
if p.suffix.lower() != ".md":
raise ValueError("reference path must target a markdown file")
return str(p)
class PersonalMcpMetadata(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
id: str
version: str
tags: list[str] = Field(default_factory=list)
capabilities: list[str] = Field(min_length=1)
depends_on: list[str] = Field(default_factory=list)
references: dict[str, ReferenceEntry] = Field(default_factory=dict)
@field_validator("id")
@classmethod
def validate_id(cls, value: str) -> str:
if not SKILL_ID_RE.fullmatch(value):
raise ValueError("id must be lowercase kebab-case and start with a letter")
return value
@field_validator("version")
@classmethod
def validate_version(cls, value: str) -> str:
if not SEMVER_RE.fullmatch(value):
raise ValueError("version must be semver")
return value
@field_validator("depends_on")
@classmethod
def validate_depends_on(cls, value: list[str]) -> list[str]:
for dep in value:
if not SKILL_ID_RE.fullmatch(dep):
raise ValueError(f"invalid depends_on skill id: {dep}")
return value
@field_validator("references")
@classmethod
def validate_reference_ids(cls, value: dict[str, ReferenceEntry]) -> dict[str, ReferenceEntry]:
for ref_id in value:
if not SKILL_ID_RE.fullmatch(ref_id):
raise ValueError(f"invalid reference id: {ref_id}")
return value
@model_validator(mode="after")
def ensure_primary_capability(self) -> "PersonalMcpMetadata":
expected = f"resource://skills/{self.id}/document"
if expected not in self.capabilities:
raise ValueError(f"capabilities must include {expected}")
return self
class SkillFrontmatter(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
# Anthropic/Agent Skills fields
name: str = Field(min_length=1, max_length=64)
description: str = Field(min_length=1, max_length=1024)
when_to_use: str | None = None
allowed_tools: str | list[str] | None = Field(default=None, alias="allowed-tools")
disallowed_tools: str | list[str] | None = Field(default=None, alias="disallowed-tools")
disable_model_invocation: bool | None = Field(default=None, alias="disable-model-invocation")
user_invocable: bool | None = Field(default=None, alias="user-invocable")
argument_hint: str | None = Field(default=None, alias="argument-hint")
arguments: str | list[str] | None = None
license: str | None = None
compatibility: str | None = None
metadata: dict[str, str] | None = None
# Repository extension block
x_personal_mcp: PersonalMcpMetadata = Field(alias="x-personal-mcp")
@field_validator("name")
@classmethod
def validate_name(cls, value: str) -> str:
if not SKILL_ID_RE.fullmatch(value):
raise ValueError("name must be lowercase kebab-case and start with a letter")
if "anthropic" in value or "claude" in value:
raise ValueError("name must not contain reserved words anthropic or claude")
return value
@model_validator(mode="after")
def cross_validate(self) -> "SkillFrontmatter":
if self.x_personal_mcp.id != self.name:
raise ValueError("x-personal-mcp.id must exactly match name")
return self
def validate_skill_frontmatter(raw: dict[str, Any], skill_dir_name: str) -> SkillFrontmatter:
model = SkillFrontmatter.model_validate(raw)
if model.name != skill_dir_name:
raise ValueError("frontmatter name must exactly match skill directory name")
return model
```
Validation behavior contract:
- Validate required core fields and relationships during registry load before FastMCP resource/tool registration.
- Allow unknown additive fields so frontmatter can evolve without blocking startup.
- Treat hard contract violations (missing required fields, invalid ids, broken required mappings) as startup errors.
- Treat non-critical compatibility issues as warnings when possible.
- Error messages should include skill path and failing field for CI readability.
Projection mode contract (for Anthropic API upload pipelines):
- Parse with `SkillFrontmatter` first.
- Emit Anthropic-safe frontmatter with standard fields only.
- Serialize repository metadata into standard `metadata` as namespaced keys.
- Preserve the canonical authored source in `x-personal-mcp`; projection output is a build artifact.
### Anthropic Upload Compatibility Rule
- Anthropic documentation guarantees behavior for standard frontmatter fields but does not explicitly guarantee handling of arbitrary unknown top-level keys.
- Therefore, publishing pipelines that target strict API compatibility should support a projection mode that emits only standard frontmatter fields for upload.
- In projection mode, repository extension metadata is serialized into the standard `metadata` field (for example as namespaced keys or JSON-encoded values), while source-of-truth authoring remains in `x-personal-mcp`.
### FastMCP Native Metadata Surfaces (Research Baseline)
Resources (`@mcp.resource` and templates) support native definition metadata:
- `name`, `description`, `mime_type`, `tags`
- `annotations` (`readOnlyHint`, `idempotentHint`)
- `icons`
- `meta` (custom metadata passed through to the MCP client resource object)
- `version`
- `enabled` (deprecated in v3; prefer server-level `mcp.enable()` / `mcp.disable()`)
Resources support runtime metadata:
- `ResourceContent.meta` (item-level)
- `ResourceResult.meta` (result-level `_meta`)
Tools (`@mcp.tool`) support native definition metadata:
- `name`, `description`, `tags`
- `annotations` (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`)
- `icons`
- `meta` (custom metadata passed through to the MCP client tool object)
- `version`
- `timeout`, `output_schema`, `run_in_thread`
- `enabled` (deprecated in v3; prefer server-level `mcp.enable()` / `mcp.disable()`)
Tools support runtime metadata:
- `ToolResult.meta` (execution-level metadata for each call)
### Frontmatter To FastMCP Mapping Contract
At server startup, map `x-personal-mcp` fields into FastMCP registration as follows:
- `x-personal-mcp.id` -> canonical URI namespace and identity checks.
- `description` -> default `description` for the primary skill document resource.
- `x-personal-mcp.tags` -> `tags` on resources/tools.
- `x-personal-mcp.version` -> `version` on resources/tools.
- `x-personal-mcp.capabilities` -> registered URI list plus catalog exposure.
- `x-personal-mcp.references[*]` -> resource templates or concrete resources with:
- `mime_type` from reference entry (or default)
- `meta` including `skill_id`, `ref_id`, and source `path`
- read-only annotations for documentation resources
- `x-personal-mcp.depends_on` -> catalog dependency graph metadata and validation checks.
### Invariants This Contract Guarantees
- Anthropic-required frontmatter stays valid for custom skill upload and Claude Code loading.
- MCP-specific metadata remains embedded in SKILL.md frontmatter, with no `metadata.yaml` sidecar.
- FastMCP registration uses only native metadata fields for resources/tools.
- Reference ids and metadata can evolve with low-friction updates while internal file layout under `references/` stays refactor-friendly.
### Non-goals For Step 2
- No URI versioning/deprecation rollout policy details (handled in Step 3).
- No migration script design from existing `metadata.yaml` files.
- No runtime caching/indexing performance tuning details.
-114
View File
@@ -1,114 +0,0 @@
**Step 3 Results: URI Contract and Compatibility Policy**
This section finalizes Step 3 by defining the canonical resource URI contract, template parameter rules, and explicit compatibility/versioning policy for URIs and reference ids.
### Step Deliverable
- Update the current `docs/` directory with the finalized Step 3 URI contract and compatibility policy content from this document.
### Canonical URI Surface (Normative)
The public, preferred URIs are:
1. `resource://catalog/skills_index`
2. `resource://catalog/skills/{skill_id}`
3. `resource://skills/{skill_id}/document`
4. `resource://skills/{skill_id}/references/{ref_id}`
5. `resource://docs/{path*}`
Contract intent:
- Catalog URIs are discovery surfaces.
- Skill URIs are primary per-skill guidance surfaces.
- Docs wildcard URI is a direct authored-markdown access surface under `docs/`.
### URI Semantics
`resource://catalog/skills_index`
- Returns a compact list of skill records for discovery.
- One entry per `skill_id`.
- Must include enough metadata for client-side selection (at minimum id, name, description, tags, capabilities).
`resource://catalog/skills/{skill_id}`
- Returns one normalized record for `skill_id`.
- Must include canonical document URI and declared reference ids.
- Returns not-found when `skill_id` does not exist.
`resource://skills/{skill_id}/document`
- Returns the canonical `SKILL.md` authored content for that skill.
- `skill_id` must match Step 1 stable id rules.
`resource://skills/{skill_id}/references/{ref_id}`
- Returns one reference document declared in the skill frontmatter references manifest.
- `ref_id` is the stable public handle for that reference document.
`resource://docs/{path*}`
- Returns authored markdown at a normalized relative path under `docs/`.
- Supports nested paths via RFC6570 wildcard expansion.
- Typical examples: `index.md`, `usage.md`, `skills/<skill-id>/SKILL.md`, `skills/<skill-id>/references/<file>.md`.
### Template Parameter and Validation Rules
`skill_id`
- Lowercase kebab-case.
- Must satisfy Step 1 stable id rules.
`ref_id`
- Lowercase kebab-case.
- Must be declared in the skills references manifest.
`path*`
- Relative POSIX path only.
- No leading slash.
- No `..` traversal segments.
- Resolves only inside `docs/`.
- This surface is markdown-only in end state (`.md` files).
### URI Versioning Policy
Default rule:
- Keep URIs unversioned by default.
- Allow URI and payload updates when they improve clarity or implementation simplicity.
Breaking-change rule:
- Breaking changes use direct replacement of the canonical URI family.
- No compatibility aliases or dual URI families are maintained in this greenfield phase.
FastMCP version metadata usage:
- Resource `version` metadata MAY be used for implementation/version discovery.
- URI readability and maintainability remain the primary contract.
### Reference ID Compatibility Policy
`ref_id` is the public identifier for a reference document, separate from file path.
Rules:
- Prefer keeping `ref_id` stable when practical.
- File paths may change without URI churn as long as the mapped `ref_id` resolves.
- If a reference is renamed, introduce a new `ref_id` and treat the old one as retired.
- Avoid reusing retired `ref_id` values for unrelated content.
### Invariants This Contract Guarantees
- One canonical URI pattern per core capability surface.
- Fast, low-friction URI evolution through direct replacement of canonical URIs.
- A single canonical catalog URI family with no alias maintenance overhead.
- Reference mappings can evolve with minimal churn.
### Non-goals For Step 3
- No implementation-specific transform wiring details (`VersionFilter`, mounts, provider composition).
- No migration script mechanics for auto-generating aliases.
- No authorization policy design for URI-level access control.
-248
View File
@@ -1,248 +0,0 @@
**Step 4 Results: Docs Registry Loader Design (importlib.resources + Fail-Fast Validation)**
This section finalizes Step 4 by defining a production-ready docs registry loader that reads packaged docs through Python resource APIs, parses SKILL.md frontmatter, validates schema and cross-links, and builds an immutable in-memory registry keyed by skill_id.
### Greenfield Framing (Normative)
This Step 4 design is for the greenfield target state:
1. No legacy metadata sidecars (`metadata.yaml`) are part of the runtime contract.
2. No dual-loader compatibility path is required.
3. Registry loading from packaged resources is the only runtime source of truth.
4. Compatibility shims are prohibited.
### Research Baseline (Python + Design Guidance)
Authoritative references used for this step:
1. Python `importlib.resources` docs (`files`, `as_file`, `Traversable` APIs)
2. Python `importlib.resources.abc` docs (`Traversable`, path traversal semantics, joinpath compatibility notes)
3. Pydantic v2 model/validation docs (`model_validate`, `ValidationError`, strictness and extra handling)
4. Python packaging guidance for including package data in wheels/sdists
Best-practice conclusions applied to this design:
1. Prefer `importlib.resources.files(<package>).joinpath(...)` over filesystem assumptions so stdio deployments from installed wheels work.
2. Treat resources as potentially non-filesystem artifacts (zip-import compatible); only use `as_file(...)` when an actual OS path is required.
3. Validate metadata with explicit Pydantic models and fail startup on contract violations.
4. Keep registry load deterministic (sorted traversal, stable error messages, no hidden fallback mutations).
5. Resolve references via manifest ids declared in frontmatter, not by global file conventions.
### Loader Responsibilities (Normative)
The Step 4 loader MUST:
1. Read canonical docs from package resources (not repo-root paths).
2. Discover all skill directories under `docs/skills/` in packaged resources.
3. For each skill, read and parse `SKILL.md` frontmatter.
4. Validate frontmatter using the Step 2 schema contract.
5. Validate directory/id invariants from Step 1 (directory name equals frontmatter id).
6. Validate URI/reference semantics from Step 3 assumptions.
7. Build a single in-memory registry keyed by `skill_id`.
8. Fail fast on any integrity error before FastMCP resource registration.
9. Precompute compact discovery projections so index resources can be served without reading full markdown bodies at request time.
### Package Resource Contract
Runtime anchor:
1. The loader resolves content from an importable package anchor, for example `personal_mcp`.
2. Docs root is located as `files(anchor).joinpath("docs")` when docs are packaged at package root, or an equivalent configured subpath.
3. Skill root is `docs/skills`.
Resource assumptions:
1. `SKILL.md` is UTF-8 text.
2. Reference files declared in frontmatter are UTF-8 markdown by default unless otherwise declared.
3. Path resolution always remains inside the same skill directory.
### Registry Data Model
Build immutable runtime records with explicit structure:
1. `SkillRecord`
- `skill_id`
- `name`
- `description`
- `version`
- `tags`
- `capabilities`
- `depends_on`
- `document_uri`
- `document_relpath` (canonical resource-relative path)
- `references` map keyed by `ref_id`
2. `ReferenceRecord`
- `ref_id`
- `uri`
- `relpath`
- `mime_type`
- `title`
3. `DocsRegistry`
- `skills_by_id: dict[str, SkillRecord]`
- `skills_in_load_order: list[str]` (deterministic ordering)
- helper indexes for catalog payload generation
- `skills_summary_in_load_order: list[SkillSummaryRecord]` for progressive discovery responses
- filter indexes (for example by tag/capability) derived once at startup
4. `SkillSummaryRecord`
- `skill_id`
- `name`
- `description`
- `tags`
- `capabilities`
- `document_uri`
- optional `version`
Immutability rule:
1. Once built, registry records are treated as read-only for the process lifetime.
2. No runtime mutation during requests; refresh only via process restart.
### Frontmatter Parsing Contract
`SKILL.md` parse steps:
1. Read full markdown text from resource.
2. Parse YAML frontmatter block at file start (between the first two `---` delimiters).
3. Parse YAML with safe loader semantics.
4. Validate parsed object with Step 2 Pydantic model(s).
5. Preserve markdown body as document content payload.
Parsing failure behavior:
1. Missing frontmatter block: startup error.
2. Invalid YAML: startup error with skill path and YAML parser detail.
3. Missing required fields (`name`, `description`, `x-personal-mcp` contract fields): startup error.
### Validation Pipeline (Fail-Fast)
Validation happens in this order:
1. Structural discovery validation
- skill directory exists under `docs/skills`
- required `SKILL.md` exists for each discovered skill
2. Schema validation
- Pydantic frontmatter validation for all required and constrained fields
3. Identity validation
- frontmatter `name` equals `x-personal-mcp.id`
- frontmatter id equals skill directory name
4. Reference manifest validation
- unique `ref_id` keys per skill
- each manifest path is relative, in-skill, and under `references/`
- each manifest target exists and is a file
5. Dependency graph validation
- every `depends_on` target exists in discovered skill set
- no self-dependency
- cycle detection enabled (hard error on cycle)
6. Capability sanity checks
- required primary capability `resource://skills/{skill_id}/document` is present
7. Global uniqueness checks
- no duplicate `skill_id`
- no duplicate canonical resource URIs generated from registry
8. Discovery payload checks
- summary fields required by catalog index are present and non-empty
- summary generation does not require reading markdown body content during request handling
### Error Model and Reporting
Error handling contract:
1. Collect errors per validation phase for clarity, then raise one startup exception containing all findings.
2. Error messages must include:
- skill id (when known)
- packaged relative path
- violated rule
- actionable fix hint
3. If any error exists, registry is not published and FastMCP resource registration does not proceed.
Recommended exception shape:
1. `DocsRegistryValidationError(errors: list[RegistryIssue])`
2. `RegistryIssue` fields: `code`, `message`, `skill_id`, `path`, `hint`
### Determinism and Runtime Safety
Determinism rules:
1. Traverse directories in sorted order.
2. Normalize all stored relative paths to POSIX form.
3. Normalize ids/tags exactly once at parse boundary.
4. Produce stable catalog ordering to reduce client churn.
5. Produce stable summary projections and filter indexes from the same normalized source records.
Runtime safety rules:
1. No dependence on `Path(__file__)` or repository root.
2. No ad-hoc fallback probing across multiple locations.
3. No lazy validation deferred until first request.
### Integration Plan for Existing Modules
Primary integration target:
1. Implement the canonical package-resource-based registry loader in `src/personal_mcp/skills/document_loader.py` as the only supported runtime loader path.
Catalog integration:
1. Update `src/personal_mcp/catalog/server.py` to consume the shared in-memory registry as the only catalog data source.
2. Keep catalog payload normalization deterministic and sourced from registry records only.
Startup wiring:
1. Initialize registry once during app/server startup in `src/personal_mcp/main.py` or equivalent composition point.
2. Pass registry to resource registration step (Step 5).
### Proposed Loader API Surface
Use a small, testable API:
1. `load_docs_registry(*, package_anchor: str, docs_root: str = "docs") -> DocsRegistry`
2. `read_skill_document(registry: DocsRegistry, skill_id: str) -> DocumentPayload`
3. `read_skill_reference(registry: DocsRegistry, skill_id: str, ref_id: str) -> DocumentPayload`
Design constraints:
1. Loader functions are pure relative to package resources and input args.
2. No global mutable singleton required for unit tests.
3. Caching is explicit and owned by startup composition.
### Test and Validation Plan (Step 4 Scope)
Unit tests:
1. valid multi-skill registry load from packaged test fixtures
2. duplicate id detection
3. missing SKILL.md detection
4. invalid frontmatter field constraints
5. broken reference target detection
6. invalid depends_on target detection
7. cycle detection in depends_on graph
8. deterministic output ordering across runs
Packaging/runtime tests:
1. install built wheel in isolated env
2. load registry via `importlib.resources.files(...)`
3. assert representative skill document/reference are readable
Expected command path in this repo:
1. `uv run pytest -q`
### Acceptance Criteria for Step 4 Completion
Step 4 is complete when all are true:
1. Registry loads exclusively from packaged resources.
2. All Step 2 and Step 3 dependent validations are enforced at startup.
3. Invalid docs state blocks startup with actionable diagnostics.
4. Registry is deterministic and immutable for runtime use.
5. Catalog and later resource registration can consume registry without direct filesystem scanning.
### Non-goals for Step 4
1. No FastMCP resource registration wiring details (Step 5).
2. No discovery-tool fallback behavior design (Step 6).
3. No final packaging/build-system migration mechanics (Step 7).
4. No backward-compat alias rollout mechanics in the greenfield baseline.
5. No compatibility layer of any kind (URI aliases, dual reads, adapter shims, or legacy schema bridges).
-221
View File
@@ -1,221 +0,0 @@
**Step 5 Results: Registry-Driven FastMCP Resource Registration (RFC6570 + Startup Safety)**
This section finalizes Step 5 by defining how FastMCP resources are registered from the Step 4 docs registry using RFC6570 URI templates, explicit metadata, and strict duplicate-registration safety.
### Greenfield Framing (Normative)
This Step 5 design is for the greenfield target state:
1. Registry-driven resources are the primary and authoritative discovery/read surface.
2. No legacy per-skill hardcoded resource registration is retained.
3. Resource contracts are defined for net-new clients and replace prior contracts without transition shims.
4. Step 6 tool fallback layers on top of this resource contract, not as a competing source of truth.
5. Breaking changes are intentional in this full-refactor phase.
### Research Baseline (FastMCP + URI Templates)
Authoritative references used for this step:
1. FastMCP Resources and Templates docs (resource decorator, template behavior)
2. FastMCP RFC6570 support docs (simple params, wildcard params, query params)
3. FastMCP duplicate handling docs (`on_duplicate_resources`)
4. FastMCP annotations guidance (`readOnlyHint`, `idempotentHint`)
Best-practice conclusions applied to this design:
1. Use URI templates for parameterized resources instead of generating N static resource handlers.
2. Use wildcard template parameters (`{path*}`) for hierarchical docs paths.
3. Set startup duplicate policy to `on_duplicate_resources="error"` to fail fast on contract collisions.
4. Set explicit `mime_type` and resource annotations for all docs resources.
5. Keep registration deterministic and sourced only from the validated Step 4 registry.
### Registration Responsibilities (Normative)
The Step 5 registration layer MUST:
1. Consume only the validated in-memory registry produced by Step 4.
2. Register canonical resource discovery surfaces and skill document/reference surfaces.
3. Use RFC6570 templates where URI patterns are parameterized.
4. Use wildcard templates where path depth is variable.
5. Attach read-only/idempotent annotations to documentation resources.
6. Set explicit MIME types for all registered resources.
7. Fail startup if duplicate URI/template keys are encountered.
### Canonical Resource Surface (from Registry)
The preferred resources registered in this phase are:
1. `resource://catalog/skills_index`
2. `resource://catalog/skills_index{?q,tag,capability,cursor,limit}` (optional filtered/paginated discovery template)
3. `resource://catalog/skills/{skill_id}`
4. `resource://skills/{skill_id}/document`
5. `resource://skills/{skill_id}/references/{ref_id}`
6. `resource://docs/{path*}`
Registration decision rules:
1. Use static resource registration for fixed singleton endpoints (for example `skills_index`).
2. Use template registration for parameterized endpoints (`{skill_id}`, `{ref_id}`) and optional discovery query templates.
3. Use wildcard template registration for hierarchical docs routing (`{path*}`).
4. Keep the singleton and query-template discovery surfaces semantically equivalent (same schema, query template adds filtering/pagination only).
### Progressive Discovery Contract
Discovery-first behavior for Step 5 resources:
1. `skills_index` returns summaries only (no embedded full SKILL.md bodies).
2. Each summary includes canonical follow-up URIs so clients can progressively fetch detail (`catalog/skills/{skill_id}` then `skills/{skill_id}/document`).
3. Filtered/paginated discovery uses RFC6570 query params (`q`, `tag`, `capability`, `cursor`, `limit`) with deterministic ordering.
4. Handlers should enforce bounded page size and return explicit continuation metadata when pagination is active.
5. Errors for unsupported filter params or invalid cursor/limit are explicit and actionable.
### RFC6570 Template Contract
Path parameters:
1. `{skill_id}` and `{ref_id}` are single-segment template params.
2. `{path*}` is a wildcard param and may capture multi-segment paths separated by `/`.
Validation contract at resource-read time:
1. `skill_id` must exist in registry.
2. `ref_id` must exist in that skills reference manifest.
3. wildcard `path*` must normalize to an allowed docs-relative markdown path.
4. invalid params return explicit not-found or validation errors (no silent fallback).
Template function signature contract:
1. Required URI params must exist as function parameters.
2. Avoid hidden implicit params not represented in template.
3. Keep template handlers side-effect free.
### Metadata and Annotation Contract
Each docs/resource registration should specify explicit metadata:
1. `mime_type`
- skill docs and references: `text/markdown`
- catalog payloads: `application/json`
2. `annotations`
- `readOnlyHint: true`
- `idempotentHint: true`
3. `tags`
- include stable categories such as `catalog`, `skill-doc`, `reference`, `docs`
4. `version`
- project-defined version from registry metadata where applicable
5. `meta`
- include normalized identifiers (for example `skill_id`, `ref_id`, `source_relpath`) when useful
### Startup Safety and Duplicate Policy
FastMCP initialization contract for this phase:
1. Construct the root server with `on_duplicate_resources="error"`.
2. Register all Step 5 resources during startup composition before serving traffic.
3. Treat duplicate registration as a hard startup failure.
Duplicate conflict classes covered:
1. static URI vs static URI collision
2. static URI vs template key collision
3. template URI vs template URI collision
4. conflicting registrations introduced by future aliases without explicit migration handling
### Registration Architecture
Use one dedicated registration module that converts registry records into FastMCP resources.
Recommended API:
1. `register_docs_resources(mcp: FastMCP, registry: DocsRegistry) -> None`
Responsibilities of `register_docs_resources`:
1. register singleton catalog resources
2. register parameterized catalog/detail templates
3. register skill document and reference templates
4. register docs wildcard template
5. apply shared annotations and MIME defaults consistently
Separation of concerns:
1. Step 4 validates and normalizes docs state.
2. Step 5 only registers handlers and reads from validated registry state.
3. Request handlers do not re-discover filesystem/package structure.
### Handler Behavior Contract
Catalog handlers:
1. `skills_index` returns compact deterministic discovery payload (summary records only) and supports progressive follow-up links.
2. `skills/{skill_id}` returns one normalized detail record or not-found.
Skill document handlers:
1. `skills/{skill_id}/document` returns canonical SKILL markdown content.
2. MIME type is always `text/markdown`.
Reference handlers:
1. `skills/{skill_id}/references/{ref_id}` resolves via frontmatter manifest mapping.
2. MIME type is explicit from manifest or defaults to `text/markdown`.
Wildcard docs handler:
1. `docs/{path*}` serves markdown docs under canonical packaged docs tree.
2. traversal outside docs root is blocked.
### Integration Plan for Existing Modules
Primary composition updates:
1. Implement registry-driven registration in [src/personal_mcp/mcp.py](src/personal_mcp/mcp.py) as the canonical resource composition path.
2. Keep [src/personal_mcp/main.py](src/personal_mcp/main.py) responsible for startup wiring order (load registry first, then register resources).
3. Use [src/personal_mcp/catalog/server.py](src/personal_mcp/catalog/server.py) as registry-backed handlers only.
Lifecycle order (required):
1. load and validate registry (Step 4)
2. initialize FastMCP with duplicate error policy
3. register all Step 5 resources/templates
4. start server
### Testing Plan (Step 5 Scope)
Unit/integration tests:
1. resource registration succeeds with valid registry
2. duplicate resource registration fails at startup
3. `skills/{skill_id}` template resolves expected record
4. `skills/{skill_id}/document` returns markdown with correct MIME
5. `skills/{skill_id}/references/{ref_id}` resolves manifest-mapped file
6. `docs/{path*}` resolves nested docs paths and blocks traversal attempts
7. all registered docs resources include `readOnlyHint` and `idempotentHint`
8. catalog payload order is deterministic
9. filtered/paginated `skills_index{?q,tag,capability,cursor,limit}` responses are deterministic and schema-compatible with the singleton index response
10. catalog index payload excludes full markdown bodies and includes follow-up URIs for progressive reads
Smoke tests:
1. list resources includes singleton and template entries
2. read representative skill doc URI and reference URI successfully
3. read representative wildcard docs URI successfully
### Acceptance Criteria for Step 5 Completion
Step 5 is complete when all are true:
1. Resource registration is fully registry-driven (no per-skill hardcoded decorators required for core docs surfaces).
2. RFC6570 templates are used for parameterized URI families, including wildcard where needed.
3. All docs resources declare explicit MIME types and read-only/idempotent annotations.
4. `on_duplicate_resources="error"` is enabled and verified by tests.
5. Startup fails safely on registration conflicts.
### Non-goals for Step 5
1. No tool fallback discovery behavior implementation (Step 6).
2. No packaging build inclusion mechanics (Step 7).
3. No CI gate expansion details (Step 9).
4. No migration shims for legacy URI aliases in the greenfield baseline.
5. No ranking-strategy implementation for discovery tools beyond what is needed to preserve deterministic resource-first discovery contracts.
6. No backward-compat resource aliases, adapter handlers, or dual registration paths.
-243
View File
@@ -1,243 +0,0 @@
**Step 6 Results: Resource-First Discovery and Tool Fallback Contract**
This section finalizes Step 6 by defining discovery behavior for clients that can attach MCP resources and the fallback behavior for clients or chat surfaces that must rely on MCP tools.
### Step Deliverable
- Update the current `docs/` directory with the finalized Step 6 discovery and fallback contract content from this document.
### Primary Source Baseline (Repository Docs)
Step 6 is based on the current project contracts in:
1. `docs/architecture.md` (resource-first architecture and catalog role)
2. `docs/usage.md` (operating flows, bounded loading, and fallback sequence)
3. `docs/copilot.md` (client capability lanes and practical fallback behavior)
4. `docs/mcp_layout.md` (shared content source and thin-tool fallback position)
5. `docs/securing.md` (read-only/public-docs security invariant)
Normative conclusions from those sources:
1. Discovery stays resource-first.
2. Tool fallback is allowed, thin, and read-only.
3. Resources and tools must resolve to the same canonical authored markdown.
4. Fallback behavior should keep context bounded and deterministic.
### FastMCP Source Baseline (Authoritative References)
Step 6 fallback behavior and compatibility-layer expectations align with:
1. [FastMCP server concepts](https://gofastmcp.com/servers/server)
2. [FastMCP resources and resource templates](https://gofastmcp.com/servers/resources)
3. [FastMCP resources-as-tools transform](https://gofastmcp.com/servers/transforms/resources-as-tools)
4. [MCP specification: resources](https://modelcontextprotocol.io/specification/latest/server/resources)
Applied conclusions for this step:
1. Resource contracts remain canonical and should be surfaced directly when clients support resource attachment.
2. Tool-first compatibility layers should wrap canonical resource reads rather than creating alternate authored-content stores.
3. URI-template-backed resource identity remains stable across direct-resource and tool-compatibility access paths.
### Client Tool-Naming Research Baseline
Authoritative and client-specific references to verify during implementation:
1. [MCP specification: tools](https://modelcontextprotocol.io/specification/latest/server/tools)
2. [MCP client concepts](https://modelcontextprotocol.io/docs/learn/client-concepts)
3. [FastMCP tools](https://gofastmcp.com/servers/tools)
4. [FastMCP resources-as-tools transform](https://gofastmcp.com/servers/transforms/resources-as-tools)
5. [VS Code MCP servers](https://code.visualstudio.com/docs/agent-customization/mcp-servers)
6. [VS Code MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)
7. [Cursor MCP documentation](https://docs.cursor.com/context/model-context-protocol)
8. [Claude Desktop local MCP server setup](https://support.anthropic.com/en/articles/10949351-getting-started-with-local-mcp-servers-on-claude-desktop)
Baseline naming conclusions:
1. MCP protocol tool identity is the server-advertised `name` returned by `tools/list` and used in `tools/call`.
2. FastMCP tool identity should be treated as the canonical server contract unless a tool is intentionally registered with an explicit alternate name.
3. Clients and host integrations may display, namespace, or internally route tool names with provider-specific prefixes, but those wrappers are not canonical server tool names.
4. Compatibility should be validated by observed `tools/list` and successful `tools/call` behavior in each target client rather than by assuming one global host naming convention.
### Discovery Priority Contract (Normative)
Preferred sequence for skill discovery and loading:
1. `resource://catalog/skills_index`
2. `resource://catalog/skills/{skill_id}`
3. `resource://skills/{skill_id}/document`
4. `resource://skills/{skill_id}/references/{ref_id}` only when needed
Rules:
1. Start from catalog discovery before loading any skill document.
2. Do not skip straight to broad document loading when catalog metadata can narrow choices first.
3. Use `resource://docs/{path*}` only for direct authored-doc access outside skill-specific flows.
### Fallback Activation Rule
Fallback is used only when the active client path cannot reliably attach MCP resources (for example, tool-only chat surfaces).
Rules:
1. Keep the same discovery order semantics as the resource path.
2. If resource attachment is available, prefer resources over tools.
3. Tool fallback must never become a second authoritative content source.
### Tool Fallback Surface (Normative)
The fallback tool surface includes:
1. `list_resources`
2. `read_resource`
3. `search_patterns`
4. `get_pattern_by_id`
5. `get_skill_document_by_id`
Canonical naming rule:
1. The server-level tool contract uses the exact registered FastMCP tool names above.
2. Clients that expose provider-prefixed names (for example, namespaced wrappers) must map those names to the canonical server tool name before invocation.
3. `catalog_get_skill_document_by_id` is not a canonical server tool name for this contract unless an explicit alias is intentionally registered.
Compatibility alias policy:
1. Prefer canonical server tool names over aliases.
2. Add server-side aliases only when a major client cannot reliably map its wrapper name back to the canonical name.
3. Any alias must be read-only, delegate to the same payload builder as the canonical tool, and be documented as compatibility-only.
4. If aliases are added, canonical and alias tools must return byte-for-byte equivalent payloads for the same input.
Fallback order:
1. call `list_resources` to inspect canonical static/template resource surfaces
2. call `read_resource` for catalog URIs and selected skill URIs
3. use thin catalog tools only when additional metadata-first narrowing is needed
Tool behavior requirements:
1. read-only and idempotent semantics
2. deterministic ordering and bounded pagination
3. explicit not-found responses (`found: false` style) where applicable
4. payloads remain schema-aligned with catalog resources
5. tool invocation examples and Copilot guidance must use canonical server tool names to avoid unknown-tool errors
### Major Client Compatibility Plan
Target clients and expected validation:
1. GitHub Copilot in VS Code
- primary path: attach MCP resources when `MCP Resources...` is available
- fallback path: call `list_resources`, `read_resource`, then canonical thin tools only when needed
- validation: confirm Copilot-visible tool inventory includes or can invoke `list_resources`, `read_resource`, `search_patterns`, `get_pattern_by_id`, and `get_skill_document_by_id`
- compatibility risk: host-generated wrapper names may differ from canonical FastMCP names; document any observed wrapper-to-canonical mapping
2. Cursor
- primary path: use the client MCP server configuration and resource/tool surfaces supported by the active Cursor version
- fallback path: prefer resource-backed tools first, then canonical thin tools
- validation: capture Cursor `tools/list` equivalent behavior and verify the canonical tool names or required host mappings
- compatibility risk: Cursor may present MCP tools through its own UI labels or internal routing names
3. Claude Desktop
- primary path: configure the local MCP server and inspect advertised tools/resources in Claude Desktop
- fallback path: invoke canonical server tool names exactly as returned by `tools/list`
- validation: run a local smoke prompt that reads `resource://catalog/skills_index` and loads one skill document through `read_resource` or `get_skill_document_by_id`
- compatibility risk: local server configuration and transport setup may fail before tool-name compatibility is tested
4. Generic MCP clients and SDK-based tests
- primary path: protocol-level `resources/list`, `resources/read`, `tools/list`, and `tools/call`
- fallback path: none beyond the canonical tool contract
- validation: automated smoke tests assert exact tool names returned by `tools/list` and successful calls for canonical names
- compatibility risk: SDK/client libraries may expose helper names that differ from raw protocol names
Implementation checklist:
1. Capture each target client's advertised tool names before adding aliases.
2. Prefer fixing documentation or client-side mapping when the server already advertises canonical names correctly.
3. Add a server-side alias only for a confirmed major-client incompatibility.
4. Add regression tests for canonical names, resource-backed tools, and any intentionally supported aliases.
5. Keep public examples centered on `list_resources`/`read_resource` and canonical thin tool names.
### Resources-As-Tools Compatibility Layer
Step 6 includes a resources-as-tools compatibility layer for clients that can call tools but not attach resources.
Rules:
1. It wraps canonical resource reads rather than re-implementing content transforms.
2. It preserves canonical URIs and metadata semantics.
3. It does not replace the minimal catalog tools listed above.
4. It is interoperability-driven and remains read-only.
### Resource/Tool Parity Contract
Resources and fallback tools must agree on identity and routing metadata.
Parity requirements:
1. `skill_id` and `ref_id` are identical across both paths.
2. canonical URIs in payloads match Step 3 URI rules.
3. skill metadata (`id`, `name`, `description`, `tags`, `capabilities`, `version`) remains consistent.
4. document payload returned by `get_skill_document_by_id` resolves to the same canonical `SKILL.md` content as `resource://skills/{skill_id}/document`.
### Relevance and Ranking Contract
Baseline matching behavior is metadata-first and deterministic.
Rules:
1. Search primarily over normalized skill metadata (id, name, description, tags).
2. Keep deterministic ordering and deterministic pagination behavior.
3. Keep ranking logic transparent and bounded for predictable client behavior.
Optional extension policy:
1. BM25/regex augmentation is allowed only when catalog/tool volume meaningfully harms token efficiency or precision.
2. Any augmentation must preserve canonical ids, URIs, and deterministic tie-breaking.
3. Any augmentation remains discovery-only and does not create alternate content payloads.
### Context-Bounding and Clarification Policy
To prevent context bloat and improve answer quality:
1. load only the most relevant skill document by default
2. load at most two skill documents in one pass unless the user explicitly asks for more
3. if confidence is low after catalog discovery, ask one clarifying question before loading additional skill documents
4. fetch references lazily and only when required
### Security and Safety Constraints
Fallback tools must preserve the project security invariant.
Rules:
1. tool surfaces stay documentation-only and read-only
2. no mutation, shell execution, secret access, or private filesystem exposure
3. all returned content remains safe to publish publicly
### Integration Boundaries
Step 6 integrates with prior steps as follows:
1. Step 4 provides the validated in-memory registry.
2. Step 5 provides canonical resource registration.
3. Step 6 adds fallback discovery/read behavior that reuses the same registry and canonical markdown sources.
Separation-of-concerns rule:
1. Catalog/resource contracts remain canonical.
2. Fallback tools are interoperability adapters, not a parallel architecture.
### Acceptance Criteria for Step 6 Completion
Step 6 is complete when all are true:
1. Resource-first discovery remains the documented and implemented default path.
2. `list_resources` and `read_resource` are available for tool-only clients.
3. Thin catalog tools remain minimal, read-only parity surfaces.
4. Fallback tool outputs map to canonical skill identities and URIs.
5. Context loading is bounded and clarifying-question behavior is documented for low-confidence cases.
6. No second content source is introduced; resources and tools resolve the same authored markdown.
### Non-goals for Step 6
1. No write or side-effecting tools.
2. No alternate authored markdown stores or duplicated skill content pipelines.
3. No guarantee that every client session exposes MCP resource attachment UI.
4. No packaging/build contract changes (handled in Step 7).
5. No CI gate expansion details (handled in later validation/governance steps).
@@ -0,0 +1,69 @@
---
name: Pytest Fill Scaffold
description: Fill scaffolded pytest test methods with assertions, fixtures, and minimal test data while preserving concise test names and one-line intent docstrings.
argument-hint: Target test file(s) under tests plus stack (pure-python, fastapi, sqlalchemy-sync, sqlalchemy-async, or mixed)
agent: agent
---
# Pytest Fill Scaffold
Use this prompt after test scaffolding exists and method names/docstrings are already in place.
## Inputs
- Target test file(s) under `tests/`.
- Stack type:
- `pure-python`
- `fastapi`
- `sqlalchemy-sync`
- `sqlalchemy-async`
- `mixed`
- Optional constraints:
- keep implementation minimal vs comprehensive
- marker lane target (`unit`, `integration`, `smoke`)
## Required References
Load these in order and use only what matches the task:
1. Core defaults: [pytest scaffolding skill](../../src/personal_mcp/docs/skills/pytesting/SKILL.md)
2. Naming/hierarchy preservation: [naming and organization](../../src/personal_mcp/docs/skills/pytesting/references/naming-and-organization.md)
3. Baseline pytest fixtures/markers: [pytest docs notes](../../src/personal_mcp/docs/skills/pytesting/references/pytest-docs.md)
4. FastAPI-specific behavior (only when needed): [fastapi testing](../../src/personal_mcp/docs/skills/pytesting/references/fastapi-testing.md)
5. SQLAlchemy-specific behavior (only when needed): [sqlalchemy testing](../../src/personal_mcp/docs/skills/pytesting/references/sqlalchemy-testing.md)
## Workflow
1. Inspect target files and treat human-reviewed docstring-only scaffolds as invariant.
2. Convert each scaffolded method into an executable test with a single behavior focus.
3. Keep one-line docstrings for class and method intent.
4. Add or refine fixtures at the nearest useful scope:
- global in `tests/conftest.py` only when broadly reusable
- subtree `conftest.py` for domain-specific fixtures
5. Assign markers consistent with cost and dependencies:
- `unit` for pure logic
- `integration` for framework/DB contracts
- `smoke` for thin critical-path checks
6. Validate in this order:
- `uv run pytest --collect-only -q`
- `uv run pytest -m unit -q` when unit tests are touched
- `uv run pytest -q` if dependencies are available
## Authoring Rules
- Prefer deterministic tests and explicit setup/teardown.
- Keep assertions precise and readable.
- Do not overfit tests to private implementation details.
- If a scaffolded class or method has only a docstring body, treat its name and hierarchy as locked.
- Do not rename, move, merge, split, or re-nest docstring-only scaffolded tests unless explicitly requested.
- Preserve existing one-line docstrings on scaffolded classes and methods unless they are factually incorrect.
- If stack details are missing and would change fixture strategy, ask one concise clarifying question before editing.
## Output Format
Return:
1. Files updated.
2. Fixture and marker decisions.
3. Which references were used and why.
4. Validation command results.
5. Risks or open questions.
+72
View File
@@ -0,0 +1,72 @@
---
name: Pytest Scaffold
description: Plan and scaffold pytest test files, class hierarchy, and concise method names for selected Python modules in this repository.
argument-hint: Target module path(s) in src plus scope (plan-only or scaffold)
agent: agent
---
# Pytest Scaffold
Use this prompt to do in one run what we have been doing manually in chat:
1. Build a naming and hierarchy plan for tests.
2. Scaffold test files and class/method skeletons.
3. Keep test method names concise because intent is carried by one-line docstrings.
## Inputs
- Target module path(s) under `src/`.
- Scope mode:
- `plan-only`
- `scaffold`
- Optional constraints:
- flattening preferences for path mapping under `tests/`
- method naming style preference
## Repository Rules To Apply
- Use [pytest scaffolding skill](../../src/personal_mcp/docs/skills/pytesting/SKILL.md) for strategy and defaults.
- Use [naming and organization reference](../../src/personal_mcp/docs/skills/pytesting/references/naming-and-organization.md) before finalizing hierarchy.
- Use `uv run pytest --collect-only -q` as structural validation.
- Default to a source-mirror style adapted to this repository:
- map selected modules to `tests/` with concise path segments when requested
- keep one test module per source module
## Execution Steps
1. Inspect current `tests/` layout and identify existing naming patterns.
2. Propose a concise hierarchy plan first:
- test file paths
- class hierarchy
- method naming pattern
- fixture placement (`tests/conftest.py` vs subtree `conftest.py`)
3. If scope mode is `scaffold`, implement the skeleton:
- create missing test modules
- create class hierarchy
- add one-line docstrings to every class and test method
- keep test method names short and behavior-focused
- treat resulting docstring-only scaffolds as human-reviewed baseline for future fill-in work
4. Validate collection with `uv run pytest --collect-only -q`.
5. Report results:
- files created or updated
- collection outcome
- any ambiguities or follow-up choices
## Class And Method Shape Defaults
- Class shape:
- `Test<PrimarySubject>` as the top-level subject class
- nested `Test<MethodOrArea>` classes when it improves context
- top-level `Test<FunctionName>` classes for standalone module functions
- Method shape:
- `test_<short_outcome>` naming
- one behavior target per method name
- one-line docstring that states the full intent
## Output Format
Return:
1. Discovery summary and references consulted.
2. Proposed or applied test tree.
3. Class and method naming map.
4. Validation command results.
5. Open questions only if they block confident completion.
+183
View File
@@ -0,0 +1,183 @@
## Goal
Build a local, self-hosted documentation knowledge base that can ingest software docs, generate embeddings, store them in SQLite, and expose high-quality retrieval through MCP tools and resources.
---
## Core Architectural Decisions
### Storage
Use:
* SQLite for metadata and document storage
* FTS5 for keyword search
* sqlite-vec for vector similarity search
Avoid a separate vector database unless scale requirements emerge.
---
### Embeddings
Use local embedding models via:
* sentence-transformers
Recommended model:
```text
BAAI/bge-base-en-v1.5
```
Store embeddings alongside document chunks.
---
### Ingestion
Primary sources:
1. Git repositories containing Markdown docs
2. Documentation websites via Crawl4AI
3. Sitemap-driven crawls when available
Pipeline:
```text
Source
Extract
Normalize
Chunk by headings
Embed
Store
```
Track content hashes so unchanged documents are skipped during reindexing.
---
### Retrieval
Implement hybrid retrieval:
```text
FTS5 keyword search
+
sqlite-vec similarity search
Candidate set
Reranker
Final results
```
Reranker:
```text
BAAI/bge-reranker-v2
```
The retriever owns all ranking logic.
---
### Public Interface
Do not expose vector search directly.
Expose a retrieval service through MCP:
```python
search_docs(query)
get_context(query)
get_doc(path)
```
The MCP layer becomes the stable API.
Clients never interact with embeddings or vectors.
---
## Repository Layout
```text
src/
├── knowledge/
│ ├── models.py
│ ├── chunking.py
│ ├── embeddings.py
│ ├── ingestion.py
│ ├── sqlite_store.py
│ ├── hybrid_search.py
│ ├── reranker.py
│ └── retrieval.py
├── sources/
│ ├── git_docs.py
│ ├── crawl4ai_docs.py
│ └── sitemap_docs.py
├── mcp_server/
│ ├── tools.py
│ └── resources.py
└── cli/
├── ingest.py
└── reindex.py
```
---
## Retrieval Flow
```text
User Query
Embed Query
FTS5 Search
+
Vector Search
Merge Results
Rerank
Return Context Bundle
```
Where a context bundle contains:
```python
ContextBundle(
passages=[...],
citations=[...],
related_docs=[...],
)
```
---
## Future Extensions
Without changing the architecture:
* Multiple documentation corpora
* Version-aware retrieval
* Code snippet indexing
* MCP resources for specific topics
* LangGraph integration
* Docker deployment
* Scheduled reindexing
The key design principle is: **treat the vector store as an internal implementation detail and expose a retrieval-oriented MCP interface instead.**
+18
View File
@@ -0,0 +1,18 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: personal-mcp entrypoint",
"type": "debugpy",
"request": "launch",
"module": "personal_mcp.__main__",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"justMyCode": true,
"env": {
"PYTHONUNBUFFERED": "1"
},
"args": [ "--port", "8766", "--reload" ]
}
]
}
+1 -7
View File
@@ -51,13 +51,7 @@
"command": "uv", "command": "uv",
"args": [ "args": [
"run", "run",
"uvicorn", "personal-mcp",
"personal_mcp.main:create_app",
"--factory",
"--host",
"127.0.0.1",
"--port",
"8000",
"--reload" "--reload"
], ],
"options": { "options": {
+25 -27
View File
@@ -1,54 +1,52 @@
FROM python:3.14-slim AS builder FROM python:3.14-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONUNBUFFERED=1 \
PYTHONUNBUFFERED=1 \ UV_SYSTEM_CERTS=1 \
UV_PYTHON_DOWNLOADS=0 \
UV_NO_MANAGED_PYTHON=1 \
UV_SYSTEM_PYTHON=1 \
UV_PROJECT_ENVIRONMENT=/usr/local \
UV_COMPILE_BYTECODE=1 \ UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \ UV_NO_DEV=1 \
UV_LOCKED=1 UV_LOCKED=1 \
UV_LINK_MODE=copy
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=zensical.toml,target=zensical.toml \ --mount=type=bind,source=zensical.toml,target=zensical.toml \
--mount=type=bind,source=docs/,target=docs/ \ --mount=type=bind,source=src/personal_mcp/docs/,target=src/personal_mcp/docs/ \
uvx zensical build uvx zensical build --clean --strict
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --no-install-project uv sync --no-install-project
# COPY --chown=appuser:appuser . /app COPY pyproject.toml uv.lock README.md /app/
COPY ./src /app/src
# RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
# uv sync --no-editable uv sync
FROM python:3.14-slim AS runtime FROM python:3.14-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1
PATH="/app/.venv/bin:$PATH" \
PERSONAL_MCP_SITE_DIR=/app/site
WORKDIR /app
EXPOSE 8765
RUN groupadd --system --gid 1001 appuser && \ RUN groupadd --system --gid 1001 appuser && \
useradd --system --uid 1001 --gid appuser appuser useradd --system --uid 1001 --gid appuser appuser
COPY --from=ghcr.io/astral-sh/uv:latest --chown=appuser:appuser /uv /uvx /bin/ WORKDIR /app
COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appuser /app/site /app/site
COPY --chown=appuser:appuser ./docs /app/docs
RUN --mount=type=cache,target=/root/.cache/uv \ RUN chown appuser:appuser /app
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ COPY --from=builder --chown=appuser:appuser /usr/local/lib /usr/local/lib
--mount=type=bind,source=src/,target=src/ \ COPY --from=builder --chown=appuser:appuser /usr/local/bin /usr/local/bin
uv sync --no-editable --refresh-package prompts COPY --from=builder --chown=appuser:appuser /app /app
USER appuser USER appuser
CMD ["uvicorn", "personal_mcp.main:create_app", "--factory", "--host", "0.0.0.0", "--port", "8765"] ENTRYPOINT ["/usr/local/bin/personal-mcp"]
+16
View File
@@ -0,0 +1,16 @@
# JSL MCP
```shell
uv run mcp-stdio
```
```shell
uv run fastmcp list --command "uv run mcp-stdio" --prompts
```
```shell
uv run fastmcp call --command "uv run mcp-stdio" \
--target authoring --prompt \
--input-json '{"artifact_type":"prompt","artifact_id":"release-notes","goal":"Create a reusable release-notes workflow."}' \
--json
```
+9 -4
View File
@@ -1,8 +1,13 @@
services: services:
personal-mcp: app:
build: image: personal-mcp:latest
context: . build: .
dockerfile: Dockerfile
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8765:8765" - "8765:8765"
environment:
PERSONAL_MCP_PORT: 8765
PERSONAL_MCP_HOST: 0.0.0.0
PERSONAL_MCP_RELOAD: 1
volumes:
- ./src:/app/src:ro
-254
View File
@@ -1,254 +0,0 @@
---
icon: lucide/library
---
# Architecture
## Overview
The platform is implemented as a resource-first MCP system with an integrated static documentation surface. The same methodology content powers both MCP resources and the published docs site.
An MCP server is a runtime that exposes machine-readable resources and tools through stable interfaces so AI clients can discover and consume context consistently. Here, the server's role is intentionally narrow: publish canonical methodology documents as resources, keep discovery predictable through a catalog layer, and serve the same source material as pre-built static documentation.
The system is complete in three layers:
1. Canonical methodology is maintained in Markdown skill documents.
2. Catalog resources provide normalized discovery.
3. Zensical builds a static site from those same Markdown sources and the FastAPI app serves it in the FastMCP runtime process.
Prompt documents under `docs/prompts/` are also indexed and exposed as first-class catalog and prompt surfaces.
This architecture is anchored by three contracts:
1. Docs-first authored content contract under `docs/` with strict per-skill ownership.
2. Standard `SKILL.md` frontmatter consumed directly by FastMCP.
3. Native `skill://` resource URIs with break-and-replace policy for contract changes.
Detailed contract pages:
1. [Content Contract](./contracts/index.md#content-contract)
2. [Frontmatter Contract](./contracts/frontmatter.md)
3. [URI Contract](./contracts/uris.md)
This architecture keeps authored content human-friendly while preserving machine-stable contracts.
## Intent
The architecture is designed to satisfy three long-term requirements:
1. Methodology must be editable as markdown by humans.
2. Agents must consume stable, discoverable resource contracts, with a minimal read-only catalog tool fallback for constrained clients.
3. Public documentation must be pre-built static output served from the application runtime without a separate docs service.
## System Model
### Pattern Modules
Each skill encapsulates one methodology domain in a docs-owned directory:
1. `docs/skills/<skill-id>/SKILL.md`
2. `docs/skills/<skill-id>/references/...`
The skill document and references are the authored source of truth; runtime code indexes and serves these files without becoming a second authored source.
Each skill publishes three native resource families:
1. `skill://<name>/SKILL.md` for primary instructions
2. `skill://<name>/_manifest` for file discovery and integrity metadata
3. `skill://<name>/{path*}` for supporting files
The main resource returns canonical Markdown. The generated manifest lists real relative paths, sizes, and SHA256 hashes so clients can load supporting material selectively.
### Prompt Modules
Prompt guidance can be authored in `docs/prompts/` using either canonical prompt directories (`docs/prompts/<prompt-id>/PROMPT.md`) or legacy markdown files during migration.
Prompt modules publish two additive surfaces:
1. prompt resources for catalog and document retrieval
2. MCP prompt objects for prompt-list/get-prompt style client workflows
This keeps authored markdown as source-of-truth while allowing clients to discover and invoke prompts directly.
### Catalog Module
The catalog publishes normalized records for prompts. Skills use FastMCP's native resource discovery and client utilities instead of a parallel catalog.
Typical catalog resources:
1. resource://catalog/prompts_index
2. resource://catalog/prompts_index{?q,tag,cursor,limit}
3. resource://catalog/prompts/{prompt_id}
Only canonical catalog resources are part of the runtime contract in this phase.
### Registry Loader
Importing the package does not read or parse documentation. The MCP server and FastAPI application factories initialize content when constructing a runnable server. The prompt/docs registry reads packaged resources through `importlib.resources.files(...)` and `Traversable` APIs; the native skills provider receives the packaged `personal_mcp/docs/skills` filesystem path.
Loader responsibilities:
1. Parse and validate prompt frontmatter.
2. Build the prompt catalog and MCP prompt objects.
3. Index authored Markdown for `resource://docs/{path*}`.
Skill loading is owned by `SkillsDirectoryProvider`, which scans the packaged skills directory and constructs native resources before the server starts serving requests.
The immutable registry is cached for the process lifetime. Each Uvicorn worker constructs and retains its own registry because worker processes do not share Python objects. Registry load failure is a server-factory startup error, not a package-import error or partial runtime warning.
### Content Sources
Content is authored in markdown under `docs/` and managed as long-form reference material. Skill documents and companion references now live under `docs/skills/`, while project-authored pages remain alongside them in the docs tree. Resource handlers expose the same authored documents through stable resource URIs.
The repository root `docs/` directory is the only authored source. The `src/personal_mcp/docs` path is a relative symlink to that directory for source-checkout and editable-install workflows; it is not a second content tree and packaging does not depend on traversing it.
For wheel builds, Hatchling's normal `src/personal_mcp` package traversal follows the relative `docs` symlink and archives its targets as regular files under `personal_mcp/docs/`. No `force-include` mapping is used because that would add the same archive paths twice. The prompt/docs registry uses [`importlib.resources.files`](https://docs.python.org/3/library/importlib.resources.html#importlib.resources.files), while `SkillsDirectoryProvider` scans the package-relative filesystem path. Neither path depends on the current working directory.
### Static Docs Surface
Static docs are built directly from two markdown source streams:
1. Project-authored docs pages
2. Skill and reference markdown pages
The merged docs tree is built by Zensical into static files and served by the FastAPI app.
Generated `site/` files are deployment assets for the human-facing static site. They are separate from the authored Markdown resources packaged under `personal_mcp/docs/`.
## Data Flow
```mermaid
flowchart TD
A[Authored Skill Directories] --> B[SkillsDirectoryProvider]
B --> C[Native Skill Resources]
D[Authored Prompts and Docs] --> E[Prompt and Docs Registry]
E --> F[Prompt Catalog and Docs Resources]
A --> G[Zensical Static Build]
D --> G
G --> H[FastAPI Static Mount]
```
## Contracts
### Metadata Contract
Each skill declares standard frontmatter in `docs/skills/<skill-id>/SKILL.md`.
For the full field-level contract, validation model, and FastMCP metadata mapping, see [Frontmatter Contract](./contracts/frontmatter.md).
Required fields:
1. name
2. description
The directory name is the provider identity and must match `name`. There is no skill catalog metadata or sidecar.
### URI Contract
Canonical resource URIs are:
For the full URI semantics, parameter validation rules, and compatibility policy, see [URI Contract](./contracts/uris.md).
1. skill://<skill_name>/SKILL.md
2. skill://<skill_name>/_manifest
3. skill://<skill_name>/<supporting_path>
4. resource://docs/{path*}
5. resource://catalog/prompts_index
6. resource://catalog/prompts_index{?q,tag,cursor,limit}
7. resource://catalog/prompts/{prompt_id}
8. resource://prompts/{prompt_id}/document
Validation rules:
1. `skill_name` is the lowercase kebab-case skill directory name.
2. `supporting_path` is a provider-validated relative path within that skill.
3. Docs `path*` resolves only to normalized Markdown paths under `docs/`.
### Resource Registration Contract
Skill resources are registered by one `SkillsDirectoryProvider`; prompt and docs resources remain registered from the validated registry.
Registration rules:
1. Use RFC6570 URI templates where appropriate.
2. Mark documentation resources as read-only and idempotent.
3. Set explicit mime types for resource responses.
4. Configure duplicate URI handling with `on_duplicate="error"` for startup safety.
This keeps runtime behavior deterministic and prevents accidental URI collisions.
### Versioning Rule
URIs are unversioned and canonical in this phase.
1. Breaking URI changes are handled as direct replacement.
2. No compatibility aliases or dual URI families are maintained.
## Static Hosting Pattern
The docs site is pre-built and served by the same FastAPI runtime process used by the MCP app.
Runtime behavior:
1. App starts.
2. FastAPI mounts the static docs output directory.
3. Requests to docs paths are served as static assets.
This provides a single deployment artifact with no runtime markdown rendering dependency.
## Advantages
### Single Source of Truth
Methodology is authored once and reused in both MCP resources and docs pages.
### High-Fidelity Agent Context
Resources expose the same canonical Markdown that humans author and review.
### Operational Simplicity
A single app process serves MCP and docs surfaces.
### Long-Term Maintainability
Markdown remains easy to review, while contracts remain stable for clients.
### Client Independence
Clients can use Ask, Edit, or Agent modes without requiring prompt-first orchestration. Prompt objects are available as an additive MCP surface, while resource retrieval remains the canonical source path. MCP affordances are still chat-surface-dependent: some clients or sessions expose resource attachment directly, while others make tool invocation the more reliable retrieval path.
## Authoring and Publishing Lifecycle
1. Update markdown reference content.
2. Keep skill `name` and directory identity aligned.
3. Build static docs with Zensical and run provider tests.
4. Package authored docs into `personal_mcp/docs/`.
5. Serve native MCP resources and the static docs mount.
## Scope and Non-Goals
In-scope:
1. Resource-first methodology delivery
2. Native FastMCP skill discovery
3. Pre-built static docs hosting in app runtime
Out-of-scope:
1. Prompt-first orchestration as the primary interface
2. Large tool inventories duplicating static guidance across skill modules
3. Separate dynamic docs service at runtime
The prompt catalog remains an independent surface. Tool-only skill clients use generic resource tools rather than a skill-specific compatibility layer.
## Example Content Inputs
Existing markdown reference sets are valid examples of authored source material for this architecture:
1. docs/skills/pytesting/references/pytest-docs.md
2. docs/skills/python-logging/references/python-logging-docs.md
3. docs/skills/python-logging/references/json-file-logging.md
4. docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
These inputs are treated as content sources, while native skill URIs and generated manifests form the machine-facing skill contract.
-130
View File
@@ -1,130 +0,0 @@
---
icon: lucide/pencil
---
# Authoring Guide
This page defines the practical workflow for maintaining skills, prompts, and project documentation while keeping root `docs/` as the only authored source.
Primary references:
1. [Skill Contract](./contracts/skill_contract.md)
2. [Prompt Contract](./contracts/prompt.md)
3. [Frontmatter Contract](./contracts/frontmatter.md)
4. [URI Contract](./contracts/uris.md)
5. [Zensical documentation skill](./skills/zensical-docs/SKILL.md)
## Source Tree Ownership
Edit content only under root `docs/`. The `src/personal_mcp/docs` path is a relative symlink for editable installs; do not author through a copied package tree.
Hatchling's normal package traversal follows `src/personal_mcp/docs` during wheel builds and archives the linked targets as regular files under `personal_mcp/docs/`. Do not add a `force-include` entry for root `docs/`; it duplicates those wheel paths. The installed package therefore gives `SkillsDirectoryProvider` a regular filesystem directory while Zensical builds the human site directly from root `docs/`.
Generated `site/` content is a build artifact and must not be edited by hand.
## Content Layout
```text
docs/
*.md
contracts/
prompts/<prompt-id>/
PROMPT.md
references/
skills/<skill-name>/
SKILL.md
references/
```
Keep skill and prompt files inside their owning directories. Relative links may cross sections, but content ownership should remain clear.
## Skill Authoring
A skill is discovered when a direct child of `docs/skills/` contains `SKILL.md`.
Required frontmatter:
```yaml
---
name: <skill-name>
description: <what the skill does and when to use it>
---
```
Rules:
1. Use lowercase kebab-case for the directory and `name`.
2. Keep `name` exactly equal to the directory name.
3. Write a specific description because clients use it for discovery.
4. Do not add `x-personal-mcp`, versions, tags, capabilities, or reference mappings.
5. Put supporting material anywhere beneath the skill directory, normally under `references/`.
6. Link supporting files from `SKILL.md` so humans and agents understand when to load them.
FastMCP recursively scans every skill file and generates `skill://<name>/_manifest`. Supporting-resource identity is the real relative path, not a synthetic reference id.
Recommended sequence:
1. Draft or revise `SKILL.md` routing guidance.
2. Add focused supporting files.
3. Verify relative links.
4. Run the provider tests and docs build.
5. Restart running servers because production uses `reload=False`.
## Prompt Authoring
Prompts remain registry-backed:
1. Keep one canonical `PROMPT.md`.
2. Align directory name, `name`, and `x-personal-mcp.id`.
3. Include `resource://prompts/<prompt-id>/document` in capabilities.
4. Define arguments beneath `x-personal-mcp.arguments`.
5. Keep long rationale and sources in `references/`.
Prompt argument names must be valid Python identifiers. Each argument accepts optional `title`, `description`, and `required`; unknown fields fail strict validation.
## Frontmatter Safety
1. Quote scalar values containing `:`.
2. Quote values with reserved YAML characters such as `#`, `{}`, `[]`, or leading `*`.
3. Use block scalars for punctuation-heavy multiline text.
4. Keep fields within the applicable skill or prompt contract.
## Writing Quality
1. Prefer focused sections and descriptive headings.
2. Link feature-level claims to authoritative sources.
3. Use relative links for internal pages.
4. Keep code examples minimal and actionable.
5. Avoid bare URLs in prose.
6. Load only supporting material relevant to the immediate task.
## Copilot Routing
Active instructions should point directly to native main resources:
1. `skill://zensical-docs/SKILL.md`
2. `skill://pytesting/SKILL.md`
3. `skill://vscode-configuration/SKILL.md`
When deeper guidance is needed, read the selected skill's `_manifest` and fetch supporting files by their listed path. Tool-only clients use `list_resources` and `read_resource` over the same URIs.
## Validation Checklist
```bash
uv run pytest tests/skills/test_provider.py tests/web/test_mcp_skills.py -q
uv run zensical build
uv run ruff check .
uv run ty check
uv run pytest
```
For packaging changes, also build and inspect an installed wheel so provider path resolution is verified outside the editable checkout.
## Navigation
When adding or moving pages:
1. update `zensical.toml`
2. keep top-level page icons in frontmatter
3. rebuild the site
4. verify internal links and navigation labels
-75
View File
@@ -1,75 +0,0 @@
---
icon: lucide/messages-square
---
# Prompt Contract
This page defines the canonical contract for prompts in the docs-first MCP architecture.
## Canonical Prompt Shape
Each prompt is one directory under `docs/prompts/`:
```mermaid
---
config:
treeView:
rowIndent: 20
lineThickness: 2
themeVariables:
treeView:
labelColor: '#FFFFFF'
lineColor: '#FFFFFF'
---
treeView-beta
"docs/"
"... (other docs)"
"prompts/"
"<prompt-id>/"
"PROMPT.md"
"references/"
"... (one or more markdown files, optional nested folders)"
```
Rules:
1. `PROMPT.md` is required for every prompt.
2. `references/` is the only place for prompt-specific supporting docs.
3. Nested folders inside `references/` are allowed so a prompt can reorganize internals without changing global architecture.
4. Prompt directories are independent ownership boundaries; no cross-prompt file writes.
## Metadata Location Constraint
1. Prompt metadata is embedded in YAML frontmatter in `PROMPT.md`.
2. No `metadata.yaml` sidecar exists in the end state.
3. Reference lookup metadata is documented and explicit: top-level `references/*.md` are auto-discovered from filenames, while `PROMPT.md` frontmatter declares overrides and nested mappings when needed.
## Prompt Id Contract
`prompt-id` is the public identifier and should satisfy all rules below:
1. Format: lowercase kebab-case only.
2. Character set: `a-z`, `0-9`, and `-`.
3. Must start with a letter.
4. No underscores, spaces, dots, or uppercase characters.
5. Directory name should equal `prompt-id` in each committed revision.
6. Frontmatter `id` should equal directory name in each committed revision.
7. Treat `prompt-id` as immutable after release; any rename is a breaking replacement and clients must move to the new id.
Valid examples:
1. `pytest-fill-scaffold`
2. `review-pr-comments`
3. `scaffold-fastapi-service`
Invalid examples:
1. `fill_pytest_scaffold`
2. `Prompt-Template`
3. `docs.prompt`
## Direct Documentation Inclusion
1. For direct API documentation, use mkdocstrings directives rather than pasting large code blocks.
2. Keep manually-authored code examples short and task-focused; large implementation excerpts are out of scope for this contract.
-118
View File
@@ -1,118 +0,0 @@
---
icon: lucide/bot
---
# Copilot MCP Mechanics
## Purpose
This page explains how GitHub Copilot in VS Code consumes native skill resources from `personal-mcp`, including sessions where tools are visible but resource attachment is not.
## Capability Lanes
Copilot interacts with MCP servers through independently exposed lanes:
1. tools invoked during execution
2. resources attached as read-only context
3. server-provided prompts
This server publishes skills as native `skill://` resources, prompts through registry-backed resources and MCP prompt objects, and generic resource fallback tools through FastMCP.
## Native Skill Resources
For every skill, Copilot can discover:
1. `skill://<name>/SKILL.md`
2. `skill://<name>/_manifest`
3. `skill://<name>/{path*}` supporting-file template
The main resource description comes from `SKILL.md`. The manifest discloses supporting paths, sizes, and SHA256 hashes. This is the only skill discovery contract; there is no parallel skill catalog.
## Resource Picker Availability
`MCP Resources...` in Add Context requires both:
1. a connected server advertising resource capability
2. a chat surface that exposes MCP resource attachment
A successful `resources/list` response does not guarantee the picker appears in every session type. Use `MCP: Browse Resources` to distinguish server availability from chat UI availability.
## Recommended Workflow
When resource attachment is available:
1. browse the server's resources
2. attach one relevant `skill://<name>/SKILL.md`
3. attach `_manifest` only if supporting detail may be needed
4. attach only selected supporting files
When only tools are available:
1. call `list_resources`
2. select a native main skill URI by name and description
3. call `read_resource` for that URI
4. read `_manifest` and supporting files only as needed
Both paths resolve through the same FastMCP provider.
## Prompt Examples
Resource attachment:
```text
Use the attached personal-mcp skill as guidance, then reconcile it with the repository before proposing changes.
```
Tool-only discovery:
```text
Call list_resources, choose the best matching skill://.../SKILL.md resource, and read it. Inspect its _manifest only if a supporting file is needed. Load at most two candidate skills.
```
Direct loading:
```text
Read skill://async-fastapi-sqlmodel/SKILL.md and apply only the sections relevant to this repository.
```
Supporting material:
```text
Read skill://pytesting/_manifest, select the one reference relevant to async test lifecycle, and use that file with the main skill instructions.
```
## Repository Instruction Pattern
A repo-level instruction should name the native retrieval order and context budget:
```md
When a task matches a personal-mcp skill:
1. Prefer an already attached native skill resource.
2. Otherwise use `list_resources` and select one `skill://<name>/SKILL.md` resource by description.
3. Read `_manifest` only when supporting material is needed.
4. Load at most two candidate main files and only the relevant supporting paths.
5. Reconcile guidance with the current repository before editing.
```
Instructions steer behavior but do not force VS Code to attach resources automatically.
## Prompt Objects
Prompt modules remain separate from skills. When the client supports MCP prompt APIs, use prompt listing and `get_prompt` for parameterized workflows. Authored `PROMPT.md` remains the source of truth for each prompt.
## Troubleshooting
1. Use `MCP: List Servers` to confirm the server is enabled.
2. Use `MCP: Browse Resources` to confirm native skill resources exist.
3. Restart the MCP server after changing skill files because production uses `reload=False`.
4. Reload the VS Code window if the server is healthy but the resource picker remains stale.
5. In tool-only sessions, verify `list_resources` and `read_resource` are visible.
## Further Reading
1. [FastMCP Skills Provider](https://gofastmcp.com/servers/providers/skills)
2. [Add and manage MCP servers](https://code.visualstudio.com/docs/agent-customization/mcp-servers)
3. [MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)
4. [Manage context for AI](https://code.visualstudio.com/docs/chat/copilot-chat-context)
5. [Skill Usage Mechanics](./usage.md)
-48
View File
@@ -1,48 +0,0 @@
---
icon: lucide/rocket
---
# Personal MCP
This project is a document library of software patterns, best practices, and structured references to external documentation. The same markdown files are published through two equivalent surfaces, so human-readable docs and MCP resources stay aligned.
## MCP Server
An [MCP server](https://modelcontextprotocol.io/docs/getting-started/intro) at `/mcp` provides context for AI systems. The markdown files are exposed as [resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) and are structured to be easily consumed by [MCP clients](https://modelcontextprotocol.io/docs/learn/client-concepts), such as VS Code.
## Docs
A website at `/docs` for humans to read and review.
## Quick start
Install dependencies first:
```bash
uv sync
```
Run the app locally with the static docs rebuilt first, using [Uvicorn factory mode](https://www.uvicorn.org/settings/#application):
```bash
uv run zensical build && uv run uvicorn personal_mcp.main:create_app --factory --host 127.0.0.1 --port 8765
```
Build and run the Docker image with the same exposed port:
```bash
docker build -t personal-mcp . && docker run --rm -p 8765:8765 personal-mcp
```
When the server is running, the health check is available at `/healthz` and the generated docs are available at `/docs/`.
## Architecture
- [Resource-First Pattern Module Architecture](./architecture.md)
- [Contracts](./contracts/index.md)
- [Content Contract](./contracts/index.md#content-contract)
- [Frontmatter Contract](./contracts/frontmatter.md)
- [URI Contract](./contracts/uris.md)
- [Static Docs Hosting Pattern](./mcp_layout.md)
- [Skill Usage Mechanics](./usage.md)
- [Copilot MCP Mechanics](./copilot.md)
-200
View File
@@ -1,200 +0,0 @@
---
icon: lucide/server
---
# Static Docs Hosting Pattern
## Purpose
This document describes the completed layout and runtime pattern used to host a pre-built static documentation site from the same FastAPI app process that runs the FastMCP server.
This design intentionally avoids runtime docs rendering and avoids a separate docs hosting service.
It also treats Markdown as the single source of truth for both MCP resources and published docs.
## Completed-State Layout
```mermaid
---
config:
treeView:
rowIndent: 40
lineThickness: 2
themeVariables:
treeView:
labelColor: '#FFFFFF'
lineColor: '#FFFFFF'
---
treeView-beta
"project-root"
"pyproject.toml"
"uv.lock"
"zensical.toml"
"docs"
"index.md"
"<project-docs>.md"
"contracts"
"index.md"
"<contract-pages>.md"
"mcp_layout.md"
"prompts"
"<prompt-id>"
"PROMPT.md"
"references"
"skills"
"<skill-id>"
"SKILL.md"
"references"
"<reference>.md"
"site"
"static build output"
"src"
"personal_mcp"
"__init__.py"
"main.py"
"mcp.py"
"catalog"
"<catalog-modules>.py"
"registry"
"<registry-modules>.py"
"web"
"<web-modules>.py"
"skills"
"<skills-modules>.py"
```
Notes:
1. docs contains both project-authored pages and the canonical skill Markdown tree.
2. site contains static build output only.
3. docs/skills contains canonical skill Markdown and reference Markdown.
4. docs/prompts contains canonical prompt Markdown used for prompt catalog and document surfaces.
5. MCP resources and docs site read from the same Markdown sources.
## Runtime Composition
The runtime process serves two surfaces:
1. MCP protocol surface from FastMCP
2. Static docs surface from FastAPI static mount
```mermaid
flowchart TD
A[Packaged Skill Directory] --> B[SkillsDirectoryProvider]
C[Packaged Prompts and Docs] --> D[Validated Registry]
B --> E[FastMCP Server]
D --> E
E --> F[MCP Transport]
E --> G[FastAPI Application]
G --> H[Static Mount /docs]
H --> I[Zensical Site Output]
```
Runtime guarantees:
1. The skills provider and prompt/docs registry initialize before resource exposure.
2. Duplicate resource and template registration fails startup (`on_duplicate="error"`).
3. Skill resources come directly from `SkillsDirectoryProvider` directory discovery.
4. Legacy per-skill Python servers, custom skill catalogs, and metadata sidecars are not part of the runtime.
## Build and Publish Flow
The docs flow is pre-build only.
1. Read authored docs pages and skill markdown sources.
2. Build static site with Zensical into site.
3. Start app and serve site directory as static files.
No runtime markdown conversion is required.
## Content Merge Pattern
The published docs site always contains both:
1. Project-authored docs pages
2. Skill Markdown content from docs/skills/*/SKILL.md and references
This ensures the public docs reflect architectural guidance and the exact Markdown served by MCP.
## Markdown-to-Resource Mapping
MCP resources map directly to canonical Markdown documents.
Example mapping model:
1. docs/skills/<skill-id>/SKILL.md -> skill://<skill-id>/SKILL.md
2. docs/skills/<skill-id>/<path> -> skill://<skill-id>/<path>
3. docs/<path>.md -> resource://docs/{path*}
Catalog discovery resources are:
1. resource://catalog/prompts_index
2. resource://catalog/prompts_index{?q,tag,cursor,limit}
3. resource://catalog/prompts/{prompt_id}
Resource registration details:
1. `skill://<skill-id>/SKILL.md` resolves to each skill's main instructions.
2. `skill://<skill-id>/_manifest` lists every skill file with size and SHA256 hash.
3. Per-skill wildcard templates resolve validated supporting-file paths.
4. `resource://docs/{path*}` resolves normalized Markdown paths under `docs/`.
When clients cannot attach MCP resources directly, `ResourcesAsTools` exposes generic `list_resources` and `read_resource` tools over the same provider resources.
## URI Compatibility Policy
1. Canonical URIs are the only supported URIs in this runtime.
2. No backward-compatibility aliases or dual registration paths are maintained.
3. Contract changes should update clients to canonical URIs directly.
## Why This Pattern
### Operational Simplicity
One application process serves both protocol and static docs surfaces.
### Deterministic Docs
Published docs are immutable static assets for a given build.
### Documentation Fidelity
The docs site and MCP resources resolve from the same Markdown sources.
### Maintainer Experience
Authors continue to work in markdown while resource contracts remain machine-consumable.
## FastAPI Static Mount Expectations
The FastAPI app is expected to:
1. Mount static directory containing Zensical output.
2. Serve index and asset files from that directory.
3. Keep docs route stable across releases.
Recommended route conventions:
1. /docs for static site root
2. /docs/* for static assets and page routes
## Update Lifecycle
For each documentation update:
1. Edit authored docs and skill markdown content.
2. Rebuild static site.
3. Restart runtime if needed.
This keeps docs publication explicit and predictable.
## Example Source Material
Existing reference docs remain valid content inputs in this pattern:
1. docs/skills/pytesting/references/pytest-docs.md
2. docs/skills/python-logging/references/python-logging-docs.md
3. docs/skills/python-logging/references/json-file-logging.md
4. docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
These are source documents, not deployment artifacts.
-139
View File
@@ -1,139 +0,0 @@
---
icon: lucide/shield-check
---
# Securing Remote Access
## Context
This project exposes two related surfaces from the same runtime:
1. a static documentation site under `/docs`
2. a Streamable HTTP MCP endpoint under `/mcp`
The same Markdown content backs both surfaces. For the current project shape, the MCP server is resource-first and primarily exposes public skill and documentation text. It is not intended to expose secrets, private data, shell access, filesystem access, or tools with side effects.
The expected deployment path is:
```text
Public internet
-> Cloudflare Tunnel
-> Caddy
-> personal-mcp container
```
## Decision
For the current use case, heavy application-level authentication is not required.
The recommended posture is:
1. Keep the service behind Cloudflare Tunnel and Caddy.
2. Do not expose the container port directly to the public internet.
3. Treat everything exposed through MCP as publishable public documentation.
4. Add stronger authentication only if the MCP surface later includes sensitive content or tools with meaningful side effects.
This keeps the deployment simple while preserving a clear upgrade path.
## Tradeoffs
### Leaving `/mcp` Public
This is acceptable if `/mcp` exposes only the same public Markdown already available through `/docs`.
Benefits:
1. lowest operational friction
2. fewer compatibility issues with MCP clients
3. no need to implement OAuth, mTLS, JWT validation, or custom auth middleware
4. consistent with the project assumption that documentation content is public
Risks:
1. random scraping, probing, or fuzzing of a machine endpoint
2. possible bandwidth or CPU nuisance traffic
3. accidental future exposure if new tools or private resources are added
4. less control over who can use the MCP endpoint
### Protecting `/mcp` With Cloudflare Access
Cloudflare Access can add a lightweight gate using GitHub, Google, one-time PIN, or service tokens.
Benefits:
1. reduces random internet traffic
2. requires little app code
3. works well for a small trusted team
4. provides logs and centralized access control
Costs:
1. browser-based login may not work with all MCP clients
2. non-browser MCP clients may need Cloudflare Access service tokens
3. adds operational configuration for a low-sensitivity endpoint
### Using mTLS
mTLS is useful when both client and server environments are tightly controlled.
Benefits:
1. strong client identity
2. good fit for service-to-service or private infrastructure
3. can be used between Cloudflare, Caddy, and the backend if desired
Costs:
1. harder certificate provisioning and rotation
2. weaker compatibility with normal MCP clients
3. unnecessary for public documentation-only content
For this project, mTLS is not the primary recommendation.
## Practical Recommendation
Use a simple public-docs posture unless the endpoint changes.
Recommended current setup:
```text
/docs public
/mcp public or lightly protected
```
If `/mcp` remains public, add only basic operational safeguards:
1. keep Cloudflare Tunnel and Caddy in front
2. avoid publishing `8765` directly
3. enable Cloudflare or Caddy rate limiting if traffic becomes noisy
4. monitor logs for unusual request volume
5. document that MCP resources must remain safe to publish
A slightly stricter setup is also reasonable:
```text
/docs public
/mcp Cloudflare Access or service token
```
This is the best option if the team wants to reduce drive-by MCP traffic without adding auth code to the application.
## Upgrade Trigger
Add real authentication before introducing any MCP capability that can:
1. read non-public files
2. access private notes or credentials
3. call upstream APIs
4. mutate data
5. run commands
6. expose environment details
7. perform expensive computation
At that point, prefer edge-level authentication first, such as Cloudflare Access, and consider proper OAuth 2.1 resource-server behavior only if broad public MCP client interoperability becomes a goal.
## Security Invariant
Everything exposed by the MCP server must be safe to publish publicly.
If that invariant stops being true, `/mcp` should be protected before the new capability is deployed.
-137
View File
@@ -1,137 +0,0 @@
---
name: nicegui
description: 'Reference hub for NiceGUI and FastAPI application structure, typed configuration, ASGI and Uvicorn startup, UI composition, styling, bindable state, interactions, troubleshooting, testing, and source documentation. Use when planning, implementing, reviewing, deploying, or debugging NiceGUI applications; load only the references relevant to the task.'
---
# NiceGUI Reference
Use this skill as a progressive reference for NiceGUI applications built with FastAPI. Start with the routing map, load only the material needed for the current question, and reconcile it with the target project's NiceGUI version and established conventions.
## When to Use
- Planning or reviewing NiceGUI application structure and FastAPI composition.
- Building or refactoring pages, components, layouts, and static assets.
- Modeling UI state with bindings or bindable dataclasses.
- Implementing forms, uploads, refreshes, live updates, or background work.
- Diagnosing UI state, concurrency, navigation, or asset problems.
- Verifying framework behavior against primary documentation.
## How to Use This Skill
1. Classify the request using the discovery map below.
2. Load the smallest relevant reference, or at most two references for a mixed concern.
3. Inspect the target repository before applying guidance; preserve its sound local patterns.
4. Check the pinned NiceGUI and integration versions before relying on version-specific APIs.
5. Validate the changed behavior with focused tests and, for UI work, relevant viewport checks.
## Progressive Discovery Map
### Application Architecture
Load [application architecture](./references/architecture.md) for:
- FastAPI app factories and lifespan ownership
- package boundaries and dependency direction
- page registration and health routes
- optional persistence, LangGraph, or mounted documentation
- async responsiveness and baseline tests
### FastAPI And Uvicorn Startup
Load [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) for:
- choosing between `ui.run()` and `ui.run_with()`
- understanding the parent FastAPI app and NiceGUI's internal app
- composing ASGI lifespan and mounted routes
- loading one typed settings snapshot for server and application configuration
- serving an app instance or factory with Uvicorn
- exposing programmatic startup through `[project.scripts]`
- reload, worker, and process-local state constraints
### Components And Styling
Load [architecture and styling](./references/architecture-and-styling.md) for:
- page, component, and service boundaries
- component extraction decisions
- Quasar props, Tailwind utilities, and custom CSS boundaries
- responsive layout and static asset conventions
- Tailwind and Quasar breakpoint scales, container queries, and responsive testing
- uniformly scaling dialogs on mobile
- preserving Quasar field proportions
- keeping detached `QSelect` menus anchored
- sizing scrollable dialog cards under CSS `zoom`
- validating zoomed controls with Playwright or a browser
### Bindable State
Load [bindable dataclasses](./references/binding-dataclasses.md) for:
- typed local UI state
- propagation and refresh behavior
- nested structures and strict bindings
- mutable defaults, performance, and version notes
### Interaction Patterns
Load [interaction patterns](./references/interaction-patterns.md) for:
- uploads and form submission
- explicit refreshes
- server-sent events and WebSockets
- background work and duplicate-submission guards
### Troubleshooting And Quality
Load [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md) for:
- upload failures and UI race conditions
- stale assets and navigation drift
- responsiveness, accessibility, reliability, and maintainability checks
### Primary Sources
Load [source documentation](./references/source-documentation.md) when:
- behavior is version-sensitive or uncertain
- an integration recommendation needs verification
- upstream NiceGUI, FastAPI, Tailwind, Quasar, SQLAlchemy, Pydantic, or LangGraph documentation is required
## Common Discovery Paths
### New Application Or Architecture Review
1. Load [application architecture](./references/architecture.md).
2. Add [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) when FastAPI owns the application or startup must be exposed as a project command.
3. Add [architecture and styling](./references/architecture-and-styling.md) only when page and component design is in scope.
### Page Or Component Work
1. Load [architecture and styling](./references/architecture-and-styling.md).
2. Add [interaction patterns](./references/interaction-patterns.md) or [bindable dataclasses](./references/binding-dataclasses.md) according to the page behavior.
### Debugging Or Production Review
1. Start with [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md).
2. Follow the symptom to one detailed reference.
3. Confirm uncertain behavior in [source documentation](./references/source-documentation.md).
## General Defaults
- Keep composition, transport, services, pages, and components directionally separated.
- Keep business logic out of UI components and event handlers.
- Avoid blocking I/O and CPU-heavy work in the UI event loop.
- Prefer event-driven updates and explicit refreshes over unrelated polling.
- Prefer Tailwind utilities, then Quasar props, then reusable component helpers; use minimal shared CSS when those are insufficient.
- Provide loading, success, and failure states for user-triggered work.
- Treat version-specific guidance as a prompt to verify the project's dependency version.
## Reference Use Contract
When applying this skill:
- return only guidance relevant to the current task
- distinguish repository facts from reference recommendations
- cite the appropriate source reference for framework-level claims
- state assumptions when application requirements are missing
- report the focused checks used to validate implementation changes
@@ -1,289 +0,0 @@
# NiceGUI Page Layout And Styling
Use this reference to structure NiceGUI pages, choose component boundaries, apply responsive layout, and introduce custom CSS without fighting Quasar's internal geometry.
## Ownership And Dependency Boundaries
Keep dependencies flowing in one direction:
- pages import components and services
- components contain presentation logic only
- services contain business logic and do not import UI
- bootstrap code mounts static assets and loads shared CSS once
Suggested module split:
```text
src/my_app/
ui/
pages/
components/
static/
services/
api/
```
Page modules should compose a route from reusable presentation and service calls. They should not own domain rules, persistence, or long-running synchronous work.
## Page Composition
Build the outer layout before styling individual controls:
1. Define the page shell and width constraints.
2. Establish responsive rows, columns, gaps, and wrapping.
3. Add semantic sections and repeated components.
4. Configure Quasar component appearance with props.
5. Add custom CSS only for behavior that props and utilities cannot express safely.
```python
with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4"):
page_header(title="Inventory")
with ui.row().classes("w-full gap-4 flex-wrap lg:flex-nowrap items-start"):
filters_panel().classes("w-full lg:w-72 shrink-0")
item_grid().classes("w-full flex-1 min-w-0")
```
Use stable width, minimum-width, and flex constraints so labels, icons, validation messages, and loaded content do not shift the surrounding layout.
## Component Extraction
Extract a presentation pattern to `ui/components/` when it appears on two or more pages or when it owns a meaningful interaction boundary. Keep one-off route layout in the page module.
```python
def card_section(title: str, content: str) -> ui.card:
with ui.card().classes("w-full max-w-md") as card:
ui.label(title).classes("text-lg font-bold")
ui.label(content).classes("text-gray-600")
return card
```
Reusable components should accept data and event callbacks rather than import page state or business services implicitly.
## Styling Decision Order
NiceGUI wraps Quasar components. Choose the styling mechanism according to what it owns:
1. Use Quasar props for component appearance, density, labels, and popup behavior.
2. Use NiceGUI `.classes()` and Tailwind utilities for width, spacing, alignment, and responsive layout.
3. Use reusable component functions for repeated visual patterns.
4. Use `.style()` for genuinely dynamic inline values.
5. Use minimal shared CSS only when props and utilities are insufficient.
Common Quasar props include:
- `outlined`
- `dense`
- `stack-label`
- `popup-content-class`
- `input-class`
- `input-style`
Avoid overriding internal selectors such as:
- `.q-field__label`
- `.q-field__native`
- `.q-field__control`
- `.q-field__input`
Quasar coordinates field height, padding, labels, values, icons, and floating-label transforms. Changing only one internal part tends to cause clipping or overlap.
## Responsive Layout
Support these layouts only:
- mobile: a single-column layout with wrapping toolbars and full-width controls
- landscape desktop: $1920 \times 1080$ with side-by-side panels where they improve scanning
- portrait desktop: $1080 \times 1920 with stacked panels or a narrow fixed sidebar
Build the mobile layout first, then add one desktop breakpoint when a row or grid needs more space. Prefer flex wrapping and fluid grids before adding another breakpoint. Use Tailwind classes for page layout and Quasar props for component behavior.
```python
with ui.row().classes('w-full flex-wrap gap-4 lg:flex-nowrap items-start'):
filters_panel().classes('w-full lg:w-72 shrink-0')
item_grid().classes('w-full flex-1 min-w-0')
```
Use `min-w-0` for flexible children, `flex-wrap` for toolbars, and `max-w-* mx-auto` to keep portrait layouts readable. Do not add device-specific component trees, container queries, or custom breakpoints unless a supported layout demonstrates a concrete failure.
## Static Assets And Shared CSS
- Mount static assets from the composition layer.
- Load shared CSS once rather than injecting it from individual pages.
- Keep custom CSS tokenized with variables and scoped to application classes.
- Avoid broad rules against Quasar internals.
- Verify mount paths, reverse-proxy rewrites, and cache behavior.
```python
from pathlib import Path
from fastapi.staticfiles import StaticFiles
STATIC_DIR = Path(__file__).parent / "ui" / "static"
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
ui.add_css((STATIC_DIR / "css" / "base.css").read_text(encoding="utf-8"))
```
## Responsive Dialog Pattern
Use whole-card scaling when a form dialog must become uniformly larger on mobile while preserving Quasar's internal proportions. Keep detached select menus unscaled and make the card itself scrollable.
### Use Normal Field Density
Normal Quasar fields are approximately `56px` high, while dense fields are approximately `40px` high. Remove `dense` when larger controls are needed.
```python
ui.input("Name").props("outlined")
ui.number("Quantity").props("outlined")
ui.select(...).props(
"outlined popup-content-class=app-item-detail-menu"
)
ui.textarea("Description").props("outlined autogrow")
```
Add a scoped class to the dialog card:
```python
ui.card().classes("app-detail-card app-item-detail-card")
```
### Scale The Complete Card
```css
:root {
--item-dialog-scale: 1;
--item-dialog-max-height: calc(100dvh - 3rem);
}
.app-item-detail-card {
width: min(50rem, 50vw);
max-height: var(--item-dialog-max-height);
overflow-y: auto;
overscroll-behavior: contain;
zoom: var(--item-dialog-scale);
}
/* Restore Quasar's baseline if a global rule overrides it. */
.app-item-detail-card .q-field,
.app-item-detail-menu {
font-size: 14px;
}
@media (max-width: 599px) {
:root {
--item-dialog-scale: 1.2;
/* 75dvh becomes 90dvh after 1.2x zoom. */
--item-dialog-max-height: 75dvh;
}
.app-item-detail-card {
width: 80vw;
}
.app-item-detail-menu {
font-size: 16.8px;
}
}
```
The main mobile tuning knob is:
```css
--item-dialog-scale: 1.2;
```
### Keep Detached Popups Unscaled
Do not apply `zoom` or `transform: scale()` to a `QSelect` popup menu. Quasar renders menus outside the dialog and positions them from the unscaled anchor geometry. Scaling the menu container afterward separates it from its field.
Avoid:
```css
.app-item-detail-card,
.app-item-detail-menu {
zoom: 1.2;
}
```
Use:
```css
.app-item-detail-card {
zoom: 1.2;
}
.app-item-detail-menu {
font-size: 16.8px;
}
```
Use `popup-content-class=app-item-detail-menu` to target the detached menu and enlarge its text without changing its coordinate system.
### Account For Zoom When Scrolling
The card's pre-zoom maximum height must account for the scale:
\[
\begin{aligned}
h_{\mathrm{pre}} &= \frac{h_{\mathrm{visible}}}{s} \\
\text{where } s &= \text{the zoom scale}
\end{aligned}
\]
For a desired visual height of `90dvh` at \(1.2\times\):
\[
\frac{90\,\mathrm{dvh}}{1.2} = 75\,\mathrm{dvh}
\]
Therefore:
```css
--item-dialog-max-height: 75dvh;
```
Apply scrolling to the card itself:
```css
.app-item-detail-card {
max-height: var(--item-dialog-max-height);
overflow-y: auto;
overscroll-behavior: contain;
}
```
This keeps the dimmed page stationary while the form scrolls.
### Match The Quasar Breakpoint
Quasar's extra-small breakpoint ends at `599.98px`. A mobile-only rule can use:
```css
@media (max-width: 599px) {
/* Mobile rules. */
}
```
Confirm custom breakpoint values against the target application's Quasar configuration.
## Validation Checklist
Check each completed page at these three viewports:
1. A representative mobile viewport, such as $390 \times 844$.
2. Landscape desktop at $1920 \times 1080$.
3. Portrait desktop at $1080 \times 1920$.
Confirm that page sections do not overlap, toolbars wrap on mobile, desktop panels use the available space without becoming excessively wide, and dialogs remain visible and scroll to their final field.
## Sources
!!! info "Primary sources"
- [NiceGUI element styling and props](https://nicegui.io/documentation/element)
- [NiceGUI binding properties](https://nicegui.io/documentation/section_binding_properties)
- [Quasar components](https://quasar.dev/vue-components)
- [Quasar field](https://quasar.dev/vue-components/field/)
- [Quasar select](https://quasar.dev/vue-components/select/)
- [Tailwind responsive design](https://tailwindcss.com/docs/responsive-design)
- [MDN `zoom`](https://developer.mozilla.org/en-US/docs/Web/CSS/zoom)
@@ -1,137 +0,0 @@
# NiceGUI Application Architecture
Load this reference for application composition, package boundaries, and optional subsystem decisions.
## Baseline Package Boundaries
- `main.py`: process entry point and app factory exposure.
- `bootstrap.py`: app composition, router wiring, page registration, and lifespan orchestration.
- `config.py`: typed settings and environment parsing.
- `logging.py`: centralized logging setup.
- `api/`: HTTP transport that delegates to services.
- `services/`: business and use-case logic.
- `ui/pages/`: route-level NiceGUI pages.
- `ui/components/`: shared presentation building blocks.
Recommended base shape:
```text
.
├─ pyproject.toml
├─ .env.example
├─ src/
│ └─ app/
│ ├─ __init__.py
│ ├─ main.py
│ ├─ bootstrap.py
│ ├─ config.py
│ ├─ logging.py
│ ├─ api/
│ │ ├─ __init__.py
│ │ └─ health.py
│ ├─ services/
│ │ ├─ __init__.py
│ │ └─ example_service.py
│ └─ ui/
│ ├─ __init__.py
│ ├─ components/
│ │ ├─ __init__.py
│ │ └─ nav.py
│ └─ pages/
│ ├─ __init__.py
│ ├─ home.py
│ ├─ dashboard.py
│ └─ about.py
└─ tests/
├─ test_health.py
└─ test_pages_registration.py
```
## Required Baseline Behavior
- FastAPI is the base ASGI app.
- `create_app()` composes routes, resources, and NiceGUI.
- Lifespan owns startup and shutdown resources.
- NiceGUI pages are modular and explicitly registered.
- FastAPI exposes a health route such as `/healthz`.
- Imports do not trigger runtime global side effects.
For the ownership relationship between a caller-created FastAPI app, `nicegui.app`, `ui.run_with()`, Uvicorn, and a packaged startup command, load [FastAPI and Uvicorn startup](./fastapi-uvicorn-startup.md).
## Dependency Direction
Prefer:
- `main/bootstrap` -> `config/logging` + `api` + `ui/pages` + `services`
- `api` -> `services`
- `ui/pages` -> `ui/components` + `services`
- `services` -> helpers, clients, and `db/` when enabled
Avoid imports from services back into API or UI modules.
## Optional Persistence
Use only when the product requires durable data.
```text
src/app/db/
├─ __init__.py
├─ base.py
├─ session.py
├─ models/
└─ repositories/
```
- Create one engine and sessionmaker per process.
- Provide request- or operation-scoped sessions with `yield`.
- Keep transaction boundaries explicit in service or repository flows.
- Never share sessions across concurrent tasks.
- Use Alembic as the schema migration source of truth.
## Optional LangGraph AI
Use only for multi-step orchestration, resumable work, streaming, or human approval.
```text
src/app/ai/
├─ state.py
├─ nodes/
├─ graphs/
├─ runtime.py
└─ contracts.py
```
- Keep graph internals outside API and UI modules.
- Invoke graphs through a service such as `services/ai_service.py`.
- Use stable thread or session IDs for resumable flows.
- Keep interrupt payloads JSON-serializable.
## Optional Mounted Docs
Use only when generated docs must be served by the application.
Suggested settings:
- `docs_enabled`
- `docs_mount_path`
- `docs_site_dir`
- `docs_require_build`
Mount docs in the composition layer, normalize the mount path, avoid route conflicts, and define behavior for missing build artifacts.
## Async And Responsiveness
- Use `async def` where a handler or service path performs I/O.
- Prefer non-blocking clients and libraries.
- Offload CPU-heavy work to worker or background execution.
- Define progress, cancellation, timeout, completion, and error states for long actions.
- Stream or chunk results when workflows are long-running or multi-step.
## Testing Minimums
- Test the FastAPI health route.
- Test page registration wiring.
- If persistence is enabled, test session lifecycle and rollback behavior.
- If AI is enabled, test happy paths and interrupt/resume behavior.
- If docs are enabled, test the mounted index route.
- For long actions, test loading, completion, and error states.
@@ -1,100 +0,0 @@
# Binding Dataclasses Deep Dive
Use this reference to model NiceGUI state with bindable dataclasses and avoid common propagation and performance pitfalls.
## Primary Sources
- NiceGUI binding docs: [binding properties](https://www.nicegui.io/documentation/section_binding_properties)
- Python dataclass docs: [dataclasses module](https://docs.python.org/3/library/dataclasses.html)
- Data class design rationale: [PEP 557](https://peps.python.org/pep-0557/)
## Bindable Dataclass Behavior
`@binding.bindable_dataclass` extends standard dataclasses by turning fields into bindable properties, allowing UI bindings to propagate when a field is assigned.
```python
from nicegui import binding, ui
@binding.bindable_dataclass
class Profile:
name: str = "Ada"
age: int = 37
profile = Profile()
ui.input("Name").bind_value(profile, "name")
ui.number("Age", min=0).bind_value(profile, "age")
ui.label().bind_text_from(profile, "name", backward=lambda name: f"User: {name}")
```
## Propagation And Performance
NiceGUI distinguishes between two link types:
- Bindable properties propagate efficiently when values are assigned.
- Active links are checked in a refresh loop.
Prefer bindable dataclasses for frequently updated form state. Keep binding transforms pure and inexpensive. If an application has many active links, tune `binding_refresh_interval` in `ui.run(...)` only after measuring the impact.
## Dataclass Modeling Rules
- Use `field(default_factory=...)` for mutable defaults.
- Avoid `frozen=True` for models edited by UI controls.
- Use `slots=True` only after confirming compatibility with inheritance and extension needs.
- Keep UI-editable fields explicit and typed.
```python
from dataclasses import field
from nicegui import binding
@binding.bindable_dataclass
class Filters:
query: str = ""
tags: list[str] = field(default_factory=list)
```
## Nested Structures
NiceGUI supports tuple paths for nested data structures.
```python
from nicegui import ui
data = {"user": {"name": "Ada"}}
ui.input("Name").bind_value(data, ("user", "name"))
ui.label().bind_text_from(data, ("user", "name"))
```
Keep nested dataclass updates explicit and predictable at the field level.
## Strictness And Refactor Safety
- Object attributes are checked by default.
- Dictionary keys are not checked by default.
- Use `strict=True` when missing dictionary keys should produce warnings.
```python
from nicegui import app, ui
ui.input().bind_value(app.storage.user, "display_name", strict=True)
```
## Common Pitfalls
- In-place mutation may not produce immediate UI synchronization. Assign the updated value back to the bound field.
- Heavy binding transforms can degrade refresh performance. Move expensive work to event handlers or services.
- State shared across unrelated pages or users can leak data. Scope models to the appropriate page, client, or user context.
## Version Checks
- `bindable_dataclass` was added in NiceGUI 2.11.0.
- Depth-first binding propagation was documented in NiceGUI 2.16.0.
- Binding `strict` behavior was documented in NiceGUI 3.0.0.
- Tuple paths for nested properties were documented in NiceGUI 3.10.0.
Verify these behaviors against the NiceGUI version pinned by the target project.
@@ -1,110 +0,0 @@
# Interaction Patterns Reference
## Reactive State
Use bindable dataclasses for local page state.
```python
from dataclasses import field
from nicegui import binding, ui
@binding.bindable_dataclass
class PageState:
selected_id: int | None = None
items: list = field(default_factory=list)
state = PageState()
ui.label().bind_text_from(state, "selected_id")
```
## File Upload Pattern
- Validate extension and size before storing.
- Delegate storage to a service method.
- Notify success and failure explicitly.
```python
async def handle_upload(e: ui.events.UploadEventArguments):
try:
if e.size > 10 * 1024 * 1024:
raise ValueError("File too large")
if not e.name.endswith(".pdf"):
raise ValueError("Only PDF allowed")
await file_service.store(e.content.read(), e.name)
ui.notify(f"Uploaded: {e.name}", type="positive")
except ValueError as err:
ui.notify(str(err), type="negative")
ui.upload(on_upload=handle_upload, auto_upload=True)
```
## Form Submission Pattern
- Bind UI inputs to dataclass fields.
- Perform validation in the service layer.
- Clear form state on success.
```python
@binding.bindable_dataclass
class FormData:
name: str = ""
email: str = ""
data = FormData()
ui.input("Name").bind_value(data, "name")
ui.input("Email").bind_value(data, "email")
async def on_submit():
try:
await user_service.create_user(name=data.name, email=data.email)
ui.notify("User created", type="positive")
data.name = data.email = ""
except ValueError as err:
ui.notify(str(err), type="negative")
ui.button("Submit").on_click(on_submit)
```
## Real-Time Updates Decision
Use SSE for one-way status streaming.
Use WebSocket for bidirectional messaging.
SSE endpoint example:
```python
@app.get("/events/status")
async def status_stream():
async def gen():
while True:
yield f"data: {await get_status()}\\n\\n"
await asyncio.sleep(1)
return StreamingResponse(gen(), media_type="text/event-stream")
```
## Background Work Pattern
- Start long jobs in FastAPI background tasks.
- Expose status via endpoint or streaming channel.
- Guard buttons against duplicate submissions during in-flight tasks.
## Explicit Refresh Pattern
Use @ui.refreshable and call refresh intentionally instead of polling unrelated state.
```python
@ui.refreshable
async def item_list():
items = await service.list()
for item in items:
ui.label(item.name)
ui.button("Refresh").on_click(lambda: item_list.refresh())
```
## Links
!!! info "Primary sources"
- [NiceGUI action events](https://nicegui.io/documentation/section_action_events)
- [FastAPI server-sent events](https://fastapi.tiangolo.com/advanced/server-sent-events/)
- [FastAPI WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
@@ -1,39 +0,0 @@
# Troubleshooting and Quality Gates
## Troubleshooting
### Upload Errors
- Validate extension and size before storage.
- Catch expected exceptions and return negative notifications.
- Log unexpected exceptions with request context.
### UI Race Conditions
- Disable triggering controls during async work.
- Remove duplicate timers and listeners targeting the same state.
- Ensure service call ordering is deterministic before render updates.
### Asset Caching
- Confirm static mount and proxy rewrite correctness.
- Add cache-busting query strings for changed assets.
- Avoid per-page CSS injection.
### Navigation and State Drift
- Avoid global mutable UI state.
- Keep state request-scoped or service-managed.
- Rehydrate page data during route load.
## Production Readiness Gate
Pass all checks before shipping:
- Structure: one-way dependencies between pages, components, and services.
- Responsiveness: UI validated at both small and large viewport widths.
- Accessibility: labels and actions are clear and readable.
- Reliability: validation and exception paths surface user feedback.
- Maintainability: repeated UI patterns are extracted; business logic remains in services.
If any check fails, return to the workflow step that owns that concern.
-98
View File
@@ -1,98 +0,0 @@
---
icon: lucide/flask-conical
---
# Testing
This page describes the current test layout and execution model for this repository.
Primary guidance sources:
- [Pytest scaffolding skill](./skills/pytesting/SKILL.md)
- [Pytest docs reference](./skills/pytesting/references/pytest-docs.md)
- [FastAPI + uv + Docker skill](./skills/fastapi-uv-docker/SKILL.md)
## Goals
1. Keep local feedback fast with deterministic tests.
2. Mirror source modules with focused test groups.
3. Keep endpoint and MCP surface checks explicit.
4. Make marker usage strict and intentional.
## Current Test Layout
Current tree:
```text
tests/
__init__.py
conftest.py
registry/
test_read.py
ingest/
conftest.py
test_current_docs.py
test_document.py
test_prompt.py
models/
test_document_validation.py
test_prompt_validation.py
test_registry_payload_models.py
skills/
test_provider.py
web/
conftest.py
test_endpoint_connections.py
test_mcp_prompts.py
test_mcp_skills.py
```
Source-to-test alignment today:
- `src/personal_mcp/registry/ingest/` -> `tests/registry/ingest/`
- `src/personal_mcp/registry/models/` -> `tests/registry/models/`
- `src/personal_mcp/skills/provider.py` -> `tests/skills/test_provider.py`
- `src/personal_mcp/web/` and MCP HTTP surface -> `tests/web/`
## Markers And Strictness
Configured markers in `pyproject.toml`:
- `unit`: fast deterministic tests with no external dependencies
- `integration`: framework or component integration tests
- `smoke`: thin critical-path checks
Pytest runs with `--strict-markers`, so any unregistered marker fails the test run.
## Fixture Layering
Fixture placement follows test scope:
1. `tests/conftest.py` for cross-suite defaults.
2. `tests/registry/ingest/conftest.py` for ingest-specific setup.
3. `tests/web/conftest.py` for web and endpoint client setup.
Prefer adding fixtures at the narrowest scope that serves more than one test.
## Command Baseline
Canonical invocation:
```bash
uv run pytest
```
Useful filtered runs:
```bash
uv run pytest --collect-only -q
uv run pytest -m unit -q
uv run pytest -m integration -q
uv run pytest -m smoke -q
```
## Adding New Tests
When adding coverage:
1. Place tests under the nearest existing module subtree (`registry/`, `skills/`, or `web/`).
2. Mirror the source path where practical.
3. Reuse existing `conftest.py` files before adding new fixture layers.
4. Add markers only when they convey execution intent, and register new markers in `pyproject.toml` first.
This keeps the suite aligned with the current architecture while preserving a fast local test loop.
-128
View File
@@ -1,128 +0,0 @@
---
icon: lucide/workflow
---
# Skill Usage Mechanics
## Purpose
This page describes how clients discover and load `personal-mcp` skills published by the [FastMCP Skills Provider](https://gofastmcp.com/servers/providers/skills).
Skills are MCP resources. The client remains responsible for selecting guidance, loading only useful supporting material, and applying it to the current workspace.
## Published Skill Surface
Each directory beneath `docs/skills/` publishes:
1. `skill://<name>/SKILL.md` for primary instructions
2. `skill://<name>/_manifest` for file discovery and integrity metadata
3. `skill://<name>/{path*}` for supporting files
The server uses `supporting_files="template"`. Main files and manifests appear in `resources/list`; supporting files stay behind per-skill wildcard templates so the resource list remains compact.
The manifest contains every skill-relative path, byte size, and SHA256 hash. References do not have synthetic ids or a separate catalog record.
Prompts remain available through prompt catalog resources, prompt document resources, and MCP prompt objects.
## Discovery Workflow
Use this bounded sequence:
1. List resources or call FastMCP `list_skills()`.
2. Compare skill names and descriptions.
3. Read one selected `skill://<name>/SKILL.md`.
4. Read `skill://<name>/_manifest` only when supporting material may be useful.
5. Fetch the minimum supporting paths needed for the task.
6. Reconcile the guidance with the actual repository code before making changes.
Do not load every skill or every supporting file up front.
## FastMCP Client Utilities
FastMCP provides native utilities in `fastmcp.utilities.skills`:
1. `list_skills(client)` discovers main skill resources.
2. `get_skill_manifest(client, name)` parses a generated manifest.
3. `download_skill(client, name, target_dir)` downloads one skill.
4. `sync_skills(client, target_dir)` downloads all advertised skills.
These utilities operate directly on the native `skill://` contract and require no repository-specific adapter.
## Tool-Only Clients
The server installs [`ResourcesAsTools`](https://gofastmcp.com/servers/transforms/resources-as-tools), which exposes generic tools:
1. `list_resources`
2. `read_resource`
A tool-only client should list resources, select a `skill://<name>/SKILL.md` URI, and read it. It can then read `_manifest` and selected supporting paths through the same tool.
There are no skill-specific search, detail, or document tools. This avoids maintaining a second discovery implementation.
## Optional Tool Search
For large tool inventories, FastMCP search transforms can reduce tool-list noise:
1. `PERSONAL_MCP_TOOL_SEARCH=none|regex|bm25` defaults to `none`.
2. `PERSONAL_MCP_TOOL_SEARCH_MAX_RESULTS=<positive int>` defaults to `5`.
3. `list_resources` and `read_resource` remain visible in search modes.
These settings filter tools, not native skill resources.
## Copilot Invocation
In VS Code, skills can arrive through:
1. explicit attachment from `Add Context > MCP Resources` or `MCP: Browse Resources`
2. generic `list_resources` and `read_resource` tool calls
3. a slash-command prompt that names a specific native skill URI
Instructions can steer retrieval, but they do not guarantee automatic resource attachment in every chat surface.
A reliable prompt for a tool-only session is:
```text
Use personal-mcp list_resources to find the best matching skill://.../SKILL.md resource. Read one selected skill, inspect its _manifest only if supporting detail is needed, and reconcile the guidance with this workspace.
```
## Thin Shim Pattern
Consumer repositories can bind file scopes to native skill resources with short `.github/instructions/*.instructions.md` files.
| `applyTo` scope | Companion docs | Primary skill resource |
| --- | --- | --- |
| `**/*.md` | [Authoring Guide](./authoring.md) | `skill://zensical-docs/SKILL.md` |
| `tests/**` | [Testing](./testing.md) | `skill://pytesting/SKILL.md` |
| `.vscode/**` | [VS Code Configuration](./skills/vscode-configuration/SKILL.md) | `skill://vscode-configuration/SKILL.md` |
Minimal shape:
```md
---
name: <scope name>
description: Route <path scope> edits to a personal-mcp skill.
applyTo: '<glob>'
---
Load `skill://<skill-name>/SKILL.md` first. Read `_manifest` and supporting files only when the task needs deeper detail. Apply the guidance to the current repository rather than treating it as generated output.
```
## Failure Recovery
When no skill is an obvious match:
1. compare the available main-resource descriptions again
2. select at most two candidates
3. read their main files, not all supporting files
4. ask one clarifying question if the choice remains ambiguous
When a supporting path fails, refresh `_manifest`; file paths are the public supporting-resource identifiers.
## Runtime Checklist
1. Confirm MCP connectivity.
2. Confirm at least one `skill://<name>/SKILL.md` resource is listed.
3. Read its `_manifest` and verify `SKILL.md` appears with a SHA256 hash.
4. Read one supporting file through its manifest path.
5. Confirm `list_resources` and `read_resource` are available for tool-only clients.
6. Keep loaded context bounded to the selected skill and relevant files.
+16 -11
View File
@@ -1,26 +1,27 @@
[project] [project]
name = "prompts" name = "personal_mcp"
version = "0.1.0" version = "2.0.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"fastapi>=0.115.0", "fastapi>=0.133.0",
"fastmcp>=3.4.4", "fastmcp==4.0.0b4",
"pydantic-settings>=2", "pydantic-settings>=2",
"python-json-logger>=4",
"pyyaml>=6.0.2", "pyyaml>=6.0.2",
"python-json-logger>=4",
"uvicorn[standard]>=0.34.0", "uvicorn[standard]>=0.34.0",
"zensical>=0.0.45", "zensical>=0.0.45",
] ]
[tool.uv]
constraint-dependencies = ["fastmcp-slim==4.0.0b4"]
[project.scripts] [project.scripts]
personal-mcp = "personal_mcp.main:main" personal-mcp = "personal_mcp.__main__:main"
mcp-stdio = "personal_mcp.mcp:run_stdio"
[build-system] [build-system]
requires = ["hatchling"] requires = ["uv_build>=0.12.7,<0.13"]
build-backend = "hatchling.build" build-backend = "uv_build"
[tool.hatch.build.targets.wheel]
packages = ["src/personal_mcp"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
@@ -30,9 +31,12 @@ dev = [
"ty>=0.0.51", "ty>=0.0.51",
] ]
test = [ test = [
"asgi-lifespan>=2.1.0",
"httpx2>=2.9.1",
"pytest>=9.1.1", "pytest>=9.1.1",
"pytest-asyncio>=1.4.0", "pytest-asyncio>=1.4.0",
"pytest-cov>=7.1.0", "pytest-cov>=7.1.0",
"pyyaml>=6.0.2",
] ]
[tool.pytest.ini_options] [tool.pytest.ini_options]
@@ -47,3 +51,4 @@ markers = [
[tool.ty.src] [tool.ty.src]
include = ["src", "tests"] include = ["src", "tests"]
exclude = ["src/personal_mcp/docs/skills/*/examples/*.py"]
+19
View File
@@ -0,0 +1,19 @@
import uvicorn
from .config import get_settings
def main(cli: bool = True) -> None:
"""Run the root MCP server."""
settings = get_settings(cli=cli)
uvicorn.run(
"personal_mcp.app:create_app",
factory=True,
host=settings.host,
port=settings.port,
reload=settings.reload,
)
if __name__ == "__main__":
main()
+112
View File
@@ -0,0 +1,112 @@
from contextlib import asynccontextmanager
from importlib.resources import as_file
from importlib.resources import files
from pathlib import Path
from fastapi import FastAPI
from fastapi import Response
from fastapi import status
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastmcp.utilities.lifespan import combine_lifespans
from .config import Settings
from .config import get_settings
from .mcp import create_mcp
def create_app(settings: Settings | None = None) -> FastAPI:
runtime_settings = settings if settings is not None else get_settings()
docs_route = runtime_settings.mounts.docs.rstrip("/") or "/docs"
mcp_route = runtime_settings.mounts.mcp.rstrip("/") or "/mcp"
mcp_app = create_mcp().http_app(
path=mcp_route,
json_response=True,
stateless_http=True,
transport="http",
)
app = FastAPI(
debug=runtime_settings.debug,
docs_url=None,
redoc_url=None,
openapi_url=None,
lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan),
)
app.state.settings = runtime_settings
async def redirect_root_to_docs() -> RedirectResponse:
return RedirectResponse(
url=docs_route,
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
app.add_api_route(
"/",
redirect_root_to_docs,
methods=["GET", "HEAD"],
include_in_schema=False,
)
app.router.routes.extend(mcp_app.routes)
return app
@asynccontextmanager
async def app_lifespan(app: FastAPI):
from . import __name__ as package_root_name
site_resource = files(package_root_name).joinpath("site")
with as_file(site_resource) as site_dir:
mount_docs(
app,
docs_route=app.state.settings.mounts.docs,
site_dir=site_dir,
)
yield
def mount_docs(app: FastAPI, *, docs_route: str, site_dir: Path) -> None:
"""Mount the pre-built static docs site, or expose a clear missing-build response."""
normalized_route = docs_route.rstrip("/") or "/docs"
docs_root = f"{normalized_route}/"
async def redirect_to_docs_root() -> RedirectResponse:
return RedirectResponse(
url=docs_root,
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
app.add_api_route(
normalized_route,
redirect_to_docs_root,
methods=["GET", "HEAD"],
include_in_schema=False,
)
if site_dir.is_dir():
app.mount(
normalized_route,
StaticFiles(directory=site_dir, html=True),
name="docs",
)
return
async def docs_not_built() -> Response:
return Response(
content=("Static docs have not been built yet. Run `uv run zensical build` before using this route."),
media_type="text/plain",
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
)
app.add_api_route(
normalized_route,
docs_not_built,
methods=["GET"],
include_in_schema=False,
)
app.add_api_route(
f"{normalized_route}/{{path:path}}",
docs_not_built,
methods=["GET"],
include_in_schema=False,
)
-11
View File
@@ -1,11 +0,0 @@
from personal_mcp.catalog.server import build_prompt_detail_payload
from personal_mcp.catalog.server import build_prompts_index_payload
from personal_mcp.catalog.server import get_prompt_by_id_payload
from personal_mcp.catalog.server import search_prompts_payload
__all__ = [
"build_prompt_detail_payload",
"build_prompts_index_payload",
"get_prompt_by_id_payload",
"search_prompts_payload",
]
-125
View File
@@ -1,125 +0,0 @@
from __future__ import annotations
from typing import Any
from personal_mcp.registry.models.registry import DocsRegistry
from personal_mcp.registry.models.registry import PromptRecord
from personal_mcp.registry.models.registry import PromptSummaryPayload
DEFAULT_LIMIT = 20
MAX_LIMIT = 100
def _prompt_matches(
prompt: PromptRecord,
*,
query: str | None,
tag: str | None,
) -> bool:
if query:
lowered = query.strip().lower()
if lowered:
haystack = " ".join(
[
prompt.prompt_id,
prompt.name,
prompt.description,
" ".join(prompt.tags),
" ".join(sorted(prompt.arguments)),
]
).lower()
terms = [term for term in lowered.replace("-", " ").split() if term]
if any(term not in haystack for term in terms):
return False
return not (tag and tag not in prompt.tags)
def build_prompts_index_payload(
registry: DocsRegistry,
*,
query: str | None = None,
tag: str | None = None,
cursor: str | None = None,
limit: int | None = None,
) -> dict[str, Any]:
normalized_limit = DEFAULT_LIMIT if limit is None else max(1, min(limit, MAX_LIMIT))
try:
start = 0 if cursor is None else max(0, int(cursor))
except ValueError as exc:
raise ValueError("cursor must be an integer string") from exc
ordered = [registry.prompts_by_id[prompt_id] for prompt_id in registry.prompts_in_load_order]
matches = [prompt for prompt in ordered if _prompt_matches(prompt, query=query, tag=tag)]
page = matches[start : start + normalized_limit]
next_cursor = start + normalized_limit
return {
"prompts": [PromptSummaryPayload.from_record(prompt).model_dump() for prompt in page],
"total": len(matches),
"cursor": str(start),
"limit": normalized_limit,
"next_cursor": str(next_cursor) if next_cursor < len(matches) else None,
}
def build_prompt_detail_payload(registry: DocsRegistry, prompt_id: str) -> dict[str, Any]:
if prompt_id not in registry.prompts_by_id:
raise KeyError(prompt_id)
prompt = registry.prompts_by_id[prompt_id]
return {
"id": prompt.prompt_id,
"name": prompt.name,
"description": prompt.description,
"version": prompt.version,
"tags": list(prompt.tags),
"capabilities": list(prompt.capabilities),
"resources": {
"document": prompt.document_uri,
},
"arguments": {
arg_name: arg.model_dump(exclude_none=True) for arg_name, arg in sorted(prompt.arguments.items())
},
}
def search_prompts_payload(
registry: DocsRegistry,
*,
query: str = "",
tags: list[str] | None = None,
skip: int = 0,
limit: int = DEFAULT_LIMIT,
) -> dict[str, Any]:
normalized_skip = max(skip, 0)
normalized_limit = max(1, min(limit, MAX_LIMIT))
requested_tags = [tag.strip() for tag in (tags or []) if tag and tag.strip()]
matches: list[PromptRecord] = []
for prompt_id in registry.prompts_in_load_order:
prompt = registry.prompts_by_id[prompt_id]
if not _prompt_matches(prompt, query=query, tag=None):
continue
if requested_tags and any(tag not in prompt.tags for tag in requested_tags):
continue
matches.append(prompt)
page = matches[normalized_skip : normalized_skip + normalized_limit]
return {
"prompts": [PromptSummaryPayload.from_record(prompt).model_dump() for prompt in page],
"total": len(matches),
"skip": normalized_skip,
"limit": normalized_limit,
}
def get_prompt_by_id_payload(registry: DocsRegistry, prompt_id: str) -> dict[str, Any]:
if prompt_id not in registry.prompts_by_id:
return {"found": False, "id": prompt_id}
return {
"found": True,
"prompt": build_prompt_detail_payload(registry, prompt_id),
}
+7 -7
View File
@@ -1,15 +1,13 @@
from functools import cache from functools import cache
from pathlib import Path from pathlib import Path
from typing import Literal
from pydantic import BaseModel from pydantic import BaseModel
from pydantic import DirectoryPath
from pydantic import Field from pydantic import Field
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from pydantic_settings import SettingsConfigDict from pydantic_settings import SettingsConfigDict
DEFAULT_ENV_FILE = Path(".env").resolve() DEFAULT_ENV_FILE = Path(".env").resolve()
_REPO_ROOT = Path(__file__).resolve().parents[2] DEFAULT_SITE_DIR = Path("site").resolve()
class Mounts(BaseModel): class Mounts(BaseModel):
@@ -24,18 +22,20 @@ class Settings(BaseSettings):
env_file=DEFAULT_ENV_FILE, env_file=DEFAULT_ENV_FILE,
env_prefix="PERSONAL_MCP_", env_prefix="PERSONAL_MCP_",
extra="ignore", extra="ignore",
cli_implicit_flags=True,
) )
debug: bool = False debug: bool = False
log_level: str = "info" log_level: str = "info"
mounts: Mounts = Field(default_factory=Mounts) mounts: Mounts = Field(default_factory=Mounts)
mcp_transport: Literal["http", "sse"] = "http" host: str = "localhost"
site_dir: DirectoryPath = Field(default=_REPO_ROOT / "site") port: int = 8080
reload: bool = True
@cache @cache
def get_settings(**overrides) -> Settings: def get_settings(*, cli: bool = False, **overrides) -> Settings:
return Settings(**overrides) return Settings(**overrides, _cli_parse_args=cli) # pyright: ignore[reportCallIssue]
def refresh_settings(**overrides): def refresh_settings(**overrides):
-1
View File
@@ -1 +0,0 @@
../../docs
+43
View File
@@ -0,0 +1,43 @@
---
icon: lucide/library
---
# Architecture
Personal MCP is a small publishing service. Markdown is written once and made available in two ways:
1. as an MCP server for AI clients
2. as a documentation website for people
## How It Fits Together
```mermaid
flowchart LR
A[Markdown in src/personal_mcp/docs] --> B[MCP resources and prompts]
A --> C[Documentation website]
B --> D[AI clients]
C --> E[Human readers]
```
The running application has two routes:
- `/mcp` is the MCP endpoint.
- `/docs/` is the pre-built documentation site.
The root URL redirects to the website.
## Content Types
The server publishes three kinds of Markdown content:
- **Skills** are reusable guidance that clients read as `skill://` resources.
- **Prompts** are parameterized workflows that clients invoke as MCP prompts.
- **Documentation** is available to clients through `resource://docs/...` and to people on the website.
FastMCP provides the MCP behavior. FastAPI hosts that server beside the static site, and Zensical builds the site from the same Markdown files.
## Source Of Truth
All authored content lives under `src/personal_mcp/docs/`. The generated `src/personal_mcp/site/` directory is build output and should not be edited by hand.
For exact file formats and URI rules, see the [content contracts](./contracts/index.md). For everyday changes, start with the [Authoring Guide](./authoring.md).
+71
View File
@@ -0,0 +1,71 @@
---
icon: lucide/pencil
---
# Authoring Guide
All authored content lives under `src/personal_mcp/docs/`. The same files feed the MCP server and the documentation website.
## Source Tree Ownership
```text
src/personal_mcp/docs/
*.md # General documentation
prompts/<prompt-id>/
PROMPT.md # One MCP prompt
skills/<skill-name>/
SKILL.md # Main skill guidance
references/ # Optional supporting material
```
Do not edit `src/personal_mcp/site/` by hand. It is generated by Zensical.
## Documentation Pages
Add general documentation as Markdown under `src/personal_mcp/docs/`. Use relative links between pages. Top-level pages need an `icon` in their frontmatter, and navigation changes belong in `zensical.toml`.
## Skill Authoring
Skills follow the [Agent Skills specification](https://agentskills.io/specification). Each skill is a directory containing a required `SKILL.md` file with YAML frontmatter and Markdown instructions:
```yaml
---
name: <skill-name>
description: <what this skill covers and when to use it>
---
```
The specification requires:
1. `name` must match the directory name, contain 1-64 lowercase letters, numbers, or hyphens, and have no leading, trailing, or consecutive hyphens.
2. `description` must contain 1-1024 characters and explain both what the skill does and when an agent should use it.
3. The body of `SKILL.md` must contain the instructions an agent needs after selecting the skill.
This repository adds two narrower conventions: names start with a letter, and frontmatter contains only the required `name` and `description`. The specification also defines optional `license`, `compatibility`, `metadata`, and experimental `allowed-tools` fields, but they are not part of this repository's current [Skill Contract](./contracts/skill_contract.md).
Write skills for progressive disclosure. Keep discovery information in the frontmatter, the main workflow in `SKILL.md`, and detailed material in focused files under `references/`. Link to supporting files with paths relative to the skill root, and avoid chains of references that require an agent to open several files before finding the useful content.
The Agent Skills specification also permits `scripts/`, `assets/`, and other supporting directories. This project is primarily a guidance library, so prefer `references/` unless the skill genuinely needs executable or static resources.
[FastMCP's Skills Provider](https://gofastmcp.com/servers/providers/skills) publishes each compliant directory as MCP resources, including the main file, a generated manifest, and any supporting files. The specification's [`skills-ref` validator](https://github.com/agentskills/agentskills/tree/main/skills-ref) can validate an individual skill before the repository-wide checks run.
## Prompt Authoring
A prompt is one `PROMPT.md` file under `src/personal_mcp/docs/prompts/<prompt-id>/`. Its `prompt` frontmatter describes the workflow and arguments; its Markdown body contains the instructions.
Use each declared argument as a `{{placeholder}}` in the body. The server validates prompt metadata and placeholders when the prompt is discovered.
The [Prompt Contract](./contracts/prompt.md) and [Frontmatter Contract](./contracts/frontmatter.md) contain the exact schema.
## Validate Changes
```bash
uv run zensical build
uv run ruff check .
uv run ty check
uv run pytest
```
Restart a running server after changing content so every surface reads the latest package data.
For detailed rules, see the [content contracts](./contracts/index.md). For writing and site features, use the [Zensical documentation skill](./skills/zensical-docs/SKILL.md).
@@ -4,7 +4,7 @@ icon: lucide/braces
# Frontmatter Contract # Frontmatter Contract
This page defines the authored frontmatter contracts for native FastMCP skills and registry-backed prompts. This page defines frontmatter ownership for native skills and prompt documentation.
## Skill Frontmatter ## Skill Frontmatter
@@ -27,38 +27,30 @@ Rules:
The provider uses the directory name as the URI identity and the frontmatter `description` as the main resource description. Repository tests enforce directory/name parity and reject extra skill frontmatter fields. The provider uses the directory name as the URI identity and the frontmatter `description` as the main resource description. Repository tests enforce directory/name parity and reject extra skill frontmatter fields.
## Prompt Frontmatter ## Prompt Documentation Frontmatter
Prompts remain registry-backed and retain repository metadata: Each prompt stores runtime metadata in a nested `prompt` mapping beside fields consumed by the static documentation site. The runtime mapping uses this shape:
```yaml ```yaml
--- ---
name: <prompt-id> icon: lucide/messages-square
description: <what the prompt does and when to use it> prompt:
x-personal-mcp: version: "1.0.0"
id: <prompt-id> description: Describe when to use the prompt.
version: <semver> tags: [example, prompts]
tags: arguments: {topic: {description: "Topic to process.", required: true, choices: [first, second]}, notes: {description: "Optional constraints.", required: false}}
- <tag>
capabilities:
- resource://prompts/<prompt-id>/document
arguments:
<argument-name>:
title: <display title>
description: <input guidance>
required: true
--- ---
``` ```
Prompt rules: Prompt rules:
1. `name`, `description`, and `x-personal-mcp` are required. 1. `version`, `description`, `tags`, and `arguments` are required; unknown fields inside `prompt` or an argument are rejected.
2. `x-personal-mcp.id`, `name`, and the prompt directory name must match. 2. The directory name supplies the prompt id. Do not add a duplicate `name` field.
3. `version` must be semantic version text. 3. Argument names must be valid identifiers and preserve their authored mapping order.
4. `capabilities` must include `resource://prompts/<prompt-id>/document`. 4. Every argument requires a non-empty `description` and explicit `required` boolean.
5. Argument names must be valid Python identifiers. 5. Optional `choices` must be a non-empty list of unique, non-empty strings.
6. Argument entries accept optional `title`, `description`, and `required` fields. 6. Markdown placeholders must exactly match the declared argument names.
7. Unknown prompt fields are rejected by the strict Pydantic registry models. 7. Top-level fields such as `icon` remain owned by the documentation site and are not runtime prompt metadata.
See the MCP [prompts concept documentation](https://modelcontextprotocol.io/docs/learn/server-concepts#prompts) and [schema reference](https://modelcontextprotocol.io/specification/latest/schema) for the protocol-level prompt shape. See the MCP [prompts concept documentation](https://modelcontextprotocol.io/docs/learn/server-concepts#prompts) and [schema reference](https://modelcontextprotocol.io/specification/latest/schema) for the protocol-level prompt shape.
@@ -70,11 +62,11 @@ Skill validation is file- and provider-oriented:
2. FastMCP parses the description and scans all files when the provider is created. 2. FastMCP parses the description and scans all files when the provider is created.
3. Repository tests enforce the stricter standard-only frontmatter and directory/name rules. 3. Repository tests enforce the stricter standard-only frontmatter and directory/name rules.
Prompt validation remains registry-oriented and fails server startup for invalid metadata, duplicate prompt ids, or malformed arguments. Prompt validation is provider- and renderer-oriented. Every list or get request reloads and validates the authored files. A malformed definition fails the request instead of publishing a partial prompt set.
## Invariants ## Invariants
1. Skills remain directly portable to tools that understand standard Agent Skills directories. 1. Skills remain directly portable to tools that understand standard Agent Skills directories.
2. Native skill discovery has no parallel catalog metadata source. 2. Native skill discovery has no parallel catalog metadata source.
3. Prompts retain the richer metadata required by their catalog and MCP prompt-object surfaces. 3. Prompts use FastMCP's native component metadata and protocol surface without a parallel catalog or Python component file.
4. All authored content remains under `docs/`. 4. All authored content remains under `src/personal_mcp/docs/`.
@@ -21,13 +21,13 @@ This page defines the authored content contract for the docs-first MCP architect
## Canonical Source Of Truth ## Canonical Source Of Truth
1. All authored Markdown lives under `docs/`. 1. All authored Markdown lives under `src/personal_mcp/docs/`.
2. MCP resources and static docs are two distribution surfaces of the same authored files. 2. MCP resources and static docs are two distribution surfaces of the same authored files.
3. No parallel authored markdown is allowed in `src/` or other package-only paths. 3. No parallel authored Markdown is allowed in a root `docs/` directory or another source tree.
## Canonical Content Shape ## Canonical Content Shape
Authored content is organized under `docs/`: Authored content is organized under `src/personal_mcp/docs/`:
```mermaid ```mermaid
--- ---
@@ -41,7 +41,7 @@ config:
lineColor: '#FFFFFF' lineColor: '#FFFFFF'
--- ---
treeView-beta treeView-beta
"docs/" "src/personal_mcp/docs/"
"*.md (top-level docs pages)" "*.md (top-level docs pages)"
"contracts/" "contracts/"
"prompt.md" "prompt.md"
@@ -59,9 +59,9 @@ treeView-beta
## File Placement And Ownership Boundaries ## File Placement And Ownership Boundaries
1. Top-level project docs stay in `docs/*.md`. 1. Top-level project docs stay in `src/personal_mcp/docs/*.md`.
2. Skill docs stay in `docs/skills/<skill-id>/...`. 2. Skill docs stay in `src/personal_mcp/docs/skills/<skill-id>/...`.
3. Prompt docs stay in `docs/prompts/<prompt-id>/...`. 3. Prompt docs stay in `src/personal_mcp/docs/prompts/<prompt-id>/...`.
4. A skill or prompt may link across sections, but must not store content in another artifact's directory. 4. A skill or prompt may link across sections, but must not store content in another artifact's directory.
5. Server and runtime code may index and serve docs, but must not be the source of authored markdown. 5. Server and runtime code may index and serve docs, but must not be the source of authored markdown.
@@ -74,7 +74,7 @@ treeView-beta
This contract guarantees: This contract guarantees:
1. One authored source tree in `docs/` for both website and MCP. 1. One authored source tree in `src/personal_mcp/docs/` for both website and MCP.
2. Skill and prompt artifacts remain path-stable within their own sections. 2. Skill and prompt artifacts remain path-stable within their own sections.
3. Cross-surface publishing remains deterministic because authored content paths are canonical. 3. Cross-surface publishing remains deterministic because authored content paths are canonical.
+84
View File
@@ -0,0 +1,84 @@
---
icon: lucide/messages-square
---
# Prompt Contract
This page defines the canonical contract for declarative prompts published through a custom [FastMCP provider](https://gofastmcp.com/servers/providers/custom).
## Canonical Prompt Shape
Each prompt is one self-describing Markdown document:
```mermaid
---
config:
treeView:
rowIndent: 20
lineThickness: 2
themeVariables:
treeView:
labelColor: '#FFFFFF'
lineColor: '#FFFFFF'
---
treeView-beta
"src/personal_mcp/docs/prompts/"
"<prompt-id>/"
"PROMPT.md"
"src/personal_mcp/prompts/"
"content.py"
"models.py"
"provider.py"
```
Rules:
1. The parent directory name defines the public prompt id.
2. The nested `prompt` frontmatter block defines version, description, tags, and arguments.
3. Argument declarations define names, descriptions, requiredness, and optional string choices.
4. The Markdown body owns the rendered prompt prose and uses `{{argument_name}}` placeholders.
5. Declared arguments and body placeholders must match exactly.
6. No Python file is added when authoring a prompt.
## Ownership Boundary
1. Each `PROMPT.md` owns both its runtime metadata and prose.
2. Python owns only generic parsing, validation, rendering, and provider behavior.
3. There is no central prompt catalog, generated signature, or metadata sidecar.
4. The provider scans direct children of packaged `personal_mcp/docs/prompts/` on each list or get request.
5. Additions, edits, and deletions become visible on the next request without restarting the server.
6. Reload is pull-based; the provider does not watch files or emit proactive change notifications.
## Prompt Id Contract
`prompt-id` is the public identifier and should satisfy all rules below:
1. Format: lowercase kebab-case only.
2. Character set: `a-z`, `0-9`, and `-`.
3. Must start with a letter.
4. No underscores, spaces, dots, or uppercase characters.
5. Directory name should equal `prompt-id` in each committed revision.
6. The provider derives the prompt name from the directory; frontmatter must not duplicate it.
7. Treat `prompt-id` as immutable after release; a rename is a breaking replacement.
Valid examples:
1. `pytest-fill-scaffold`
2. `review-pr-comments`
3. `scaffold-fastapi-service`
Invalid examples:
1. `fill_pytest_scaffold`
2. `Prompt-Template`
3. `docs.prompt`
## Rendering Contract
1. The loader requires one leading YAML frontmatter block and validates its nested `prompt` mapping strictly.
2. All MCP arguments are strings; `choices` optionally restricts accepted values.
3. Missing required arguments, unknown arguments, and invalid choices fail before rendering.
4. An omitted optional value renders as `Not provided`.
5. Unknown prompt ids, malformed metadata, and mismatched placeholders fail immediately.
6. Prompt content is read through [importlib resources](https://docs.python.org/3/library/importlib.resources.html) and does not depend on the working directory.
@@ -8,7 +8,7 @@ This page defines the canonical contract for skills in the docs-first MCP archit
## Canonical Skill Shape ## Canonical Skill Shape
Each skill is one directory under `docs/skills/`: Each skill is one directory under `src/personal_mcp/docs/skills/`:
```mermaid ```mermaid
--- ---
@@ -22,7 +22,7 @@ config:
lineColor: '#FFFFFF' lineColor: '#FFFFFF'
--- ---
treeView-beta treeView-beta
"docs/" "src/personal_mcp/docs/"
"... (other docs)" "... (other docs)"
"skills/" "skills/"
"<skill-id>/" "<skill-id>/"
@@ -70,7 +70,7 @@ Invalid examples:
## Provider Publication ## Provider Publication
[`SkillsDirectoryProvider`](https://gofastmcp.com/servers/providers/skills) scans `docs/skills/` with `supporting_files="template"` and publishes: [`SkillsDirectoryProvider`](https://gofastmcp.com/servers/providers/skills) scans packaged `personal_mcp/docs/skills/` with `supporting_files="template"` and publishes:
1. `skill://<skill-id>/SKILL.md` 1. `skill://<skill-id>/SKILL.md`
2. `skill://<skill-id>/_manifest` 2. `skill://<skill-id>/_manifest`
@@ -4,7 +4,7 @@ icon: lucide/link
# URI Contract # URI Contract
This page defines the public resource URI contract for native skills, registry-backed prompts, and general authored documentation. This page defines the public resource URI contract for native skills and general authored documentation.
## Native Skill URIs ## Native Skill URIs
@@ -44,17 +44,11 @@ skill://pytesting/references/pytest-docs.md
FastMCP confines reads to the selected skill directory. Absolute paths, traversal outside the directory, missing files, directories, and symlinks that resolve outside the skill root are rejected. FastMCP confines reads to the selected skill directory. Absolute paths, traversal outside the directory, missing files, directories, and symlinks that resolve outside the skill root are rejected.
## Prompt And Docs URIs ## General Docs URI
Prompts and general documentation retain the existing registry-backed resource surface: General authored documentation is exposed through `resource://docs/{path*}`. The wildcard accepts normalized relative POSIX Markdown paths beneath packaged `personal_mcp/docs/`, excludes the provider-owned `skills/` subtree, and rejects absolute paths, traversal segments, backslashes, and non-Markdown targets.
1. `resource://catalog/prompts_index` Prompts are MCP prompt components rather than resources. Clients discover them with the protocol `prompts/list` operation and render them with `prompts/get`.
2. `resource://catalog/prompts_index{?q,tag,cursor,limit}`
3. `resource://catalog/prompts/{prompt_id}`
4. `resource://prompts/{prompt_id}/document`
5. `resource://docs/{path*}`
Prompt ids remain lowercase kebab-case. The docs wildcard accepts normalized relative POSIX Markdown paths beneath `docs/` and rejects absolute paths, traversal segments, backslashes, and non-Markdown targets.
## Discovery Order ## Discovery Order
@@ -66,11 +60,11 @@ For skills:
4. read `_manifest` when supporting material may be needed 4. read `_manifest` when supporting material may be needed
5. fetch only the supporting paths relevant to the task 5. fetch only the supporting paths relevant to the task
For prompts, use the prompt catalog or MCP prompt-object APIs. For prompts, use the native MCP prompt APIs.
## Compatibility Policy ## Stability Policy
The native `skill://` family directly replaces the repository's former custom skill URI and catalog surfaces. No compatibility aliases or dual registrations are maintained. Prompt and general-doc URIs are unaffected. The provider and protocol surfaces documented here are the complete public contract. Contract changes replace the affected surface directly.
Skill renames are breaking because the directory name is part of every native skill URI. Supporting-file renames change the corresponding manifest path and URI. Skill renames are breaking because the directory name is part of every native skill URI. Supporting-file renames change the corresponding manifest path and URI.
@@ -80,3 +74,4 @@ Skill renames are breaking because the directory name is part of every native sk
2. [MCP resources](https://modelcontextprotocol.io/specification/latest/server/resources) 2. [MCP resources](https://modelcontextprotocol.io/specification/latest/server/resources)
3. [RFC 3986 URI syntax](https://www.rfc-editor.org/rfc/rfc3986) 3. [RFC 3986 URI syntax](https://www.rfc-editor.org/rfc/rfc3986)
4. [RFC 6570 URI templates](https://www.rfc-editor.org/rfc/rfc6570) 4. [RFC 6570 URI templates](https://www.rfc-editor.org/rfc/rfc6570)
5. [FastMCP prompts](https://gofastmcp.com/servers/prompts)
+57
View File
@@ -0,0 +1,57 @@
---
icon: lucide/bot
---
# Using With GitHub Copilot
Once Personal MCP is configured as a VS Code MCP server, Copilot can use its resources, prompts, and read-only tools.
For general connection details, see [Using Personal MCP](./usage.md). For VS Code setup options, see [Add and manage MCP servers](https://code.visualstudio.com/docs/agent-customization/mcp-servers).
## Skills And Documentation
Use **MCP: Browse Resources** to inspect the server's resources. Skills appear as `skill://<name>/SKILL.md`; general pages appear under `resource://docs/...`.
For a task that needs guidance:
1. choose the skill whose description best matches the task
2. read its main `SKILL.md`
3. read `_manifest` only when supporting material is needed
4. attach or read only the relevant supporting files
When the current chat surface supports MCP resource attachments, the same resources are available from **Add Context**.
## Prompts
Personal MCP prompts appear as `/<server>.<prompt>` chat commands. Select a command and fill in its arguments to start the workflow. Arguments with a fixed list of choices offer completion as you type.
## Automatic Use
Copilot can use four fallback tools when the chat surface does not expose resources or prompts directly:
- `list_resources` and `read_resource`
- `list_prompts` and `get_prompt`
These tools access the same content as the native features. A repository instruction can guide Copilot toward the intended order:
```text
When a task matches a personal-mcp skill:
1. Prefer an attached skill resource, or browse resources and choose one by description.
2. Read its main file and load supporting material only when needed.
3. Reconcile the guidance with the current repository before editing.
```
Instructions guide resource use but do not force VS Code to attach resources automatically.
## Troubleshooting
1. Use `MCP: List Servers` to confirm the server is enabled.
2. Use `MCP: Browse Resources` to confirm resources are available.
3. Restart the MCP server after changing its content.
4. Reload the VS Code window if the server is healthy but the resource or tool list remains stale.
## Further Reading
1. [VS Code MCP configuration](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)
2. [Managing context in VS Code](https://code.visualstudio.com/docs/chat/copilot-chat-context)
3. [Using Personal MCP](./usage.md)
+49
View File
@@ -0,0 +1,49 @@
---
icon: lucide/rocket
---
# Personal MCP
Personal MCP is a library of software development guidance for people and AI assistants. Content is written once in Markdown and published as both a documentation website and an MCP server.
## What It Provides
- **Skills** provide focused guidance for development tasks.
- **Prompts** provide reusable workflows with named inputs.
- **Documentation** makes the same material easy to browse and maintain.
The HTTP service exposes the [MCP](https://modelcontextprotocol.io/docs/getting-started/intro) endpoint at `/mcp` and the website at `/docs/`.
## Quick Start
Install dependencies, build the website, and start the server:
```bash
uv sync
uv run zensical build
uv run personal-mcp --host 127.0.0.1 --port 8765
```
Then open `http://127.0.0.1:8765/docs/` or connect an MCP client to `http://127.0.0.1:8765/mcp`.
The MCP server can also run over standard input and output:
```bash
uv run mcp-stdio
```
For Docker:
```bash
docker compose up --build
```
## Read Next
- [Using Personal MCP](./usage.md)
- [Authoring Guide](./authoring.md)
- [Architecture](./architecture.md)
- [Running the Server](./mcp_layout.md)
- [Testing](./testing.md)
- [Security](./securing.md)
- [Content Contracts](./contracts/index.md)
+44
View File
@@ -0,0 +1,44 @@
---
icon: lucide/server
---
# Running The Server
Personal MCP can run as an HTTP service or as a local stdio process.
## Local HTTP Server
Build the website before starting the application:
```bash
uv sync
uv run zensical build
uv run personal-mcp --host 127.0.0.1 --port 8765
```
The server then provides:
- `http://127.0.0.1:8765/docs/` for the website
- `http://127.0.0.1:8765/mcp` for MCP clients
The host, port, log level, debug mode, and reload behavior can be set with command-line options or `PERSONAL_MCP_` environment variables.
## Local Stdio Server
For clients that manage the server process themselves:
```bash
uv run mcp-stdio
```
This mode provides MCP only; it does not host the website.
## Docker
The included Compose configuration builds the website into the image and publishes the service on port `8765`:
```bash
docker compose up --build
```
For a remote deployment, place the service behind a reverse proxy and review the [security guidance](./securing.md).
@@ -1,33 +1,21 @@
--- ---
name: authoring icon: lucide/messages-square
description: Provide a practical checklist and baseline template for authoring docs-first MCP modules and repository-specific Copilot instruction shims. prompt:
x-personal-mcp: version: "1.0.0"
id: authoring description: Provide a practical checklist and baseline template for authoring docs-first MCP modules and repository-specific Copilot instruction shims.
version: 1.0.0 tags: [authoring, mcp, fastmcp, copilot, prompts, scaffolding]
tags:
- authoring
- mcp
- fastmcp
- copilot
- prompts
- scaffolding
capabilities:
- resource://prompts/authoring/document
arguments: arguments:
artifact_type: artifact_type:
title: Artifact type description: Artifact type to create.
description: "Enum (case-sensitive): skill | prompt | shim."
required: true required: true
choices: [skill, prompt, shim]
artifact_id: artifact_id:
title: Artifact id
description: Lowercase kebab-case id for the module or shim. description: Lowercase kebab-case id for the module or shim.
required: true required: true
goal: goal:
title: Goal description: One-sentence capability statement.
description: One-sentence capability statement describing what to create and when to use it.
required: true required: true
scope_glob: scope_glob:
title: Scope glob
description: Optional applyTo glob for shim outputs. description: Optional applyTo glob for shim outputs.
required: false required: false
--- ---
@@ -36,6 +24,13 @@ x-personal-mcp:
Use this prompt to author or update docs-first MCP modules in this repository, including repository-specific Copilot thin shims. Use this prompt to author or update docs-first MCP modules in this repository, including repository-specific Copilot thin shims.
## Supplied Inputs
- `artifact_type`: {{artifact_type}}
- `artifact_id`: {{artifact_id}}
- `goal`: {{goal}}
- `scope_glob`: {{scope_glob}}
## Inputs ## Inputs
1. artifact_type: one of skill, prompt, shim 1. artifact_type: one of skill, prompt, shim
@@ -51,7 +46,7 @@ Load only what matches the requested artifact:
2. Prompt metadata and structure: [Prompt Contract](../../contracts/prompt.md) 2. Prompt metadata and structure: [Prompt Contract](../../contracts/prompt.md)
3. Skill metadata and structure (only for skill outputs): [Skill Contract](../../contracts/skill_contract.md) 3. Skill metadata and structure (only for skill outputs): [Skill Contract](../../contracts/skill_contract.md)
4. Thin shim mechanics and path binding: [Skill Usage Mechanics](../../usage.md) 4. Thin shim mechanics and path binding: [Skill Usage Mechanics](../../usage.md)
5. Copilot resource attachment and fallback behavior: [Copilot MCP Mechanics](../../copilot.md) 5. Copilot resource attachment behavior: [Copilot MCP Mechanics](../../copilot.md)
## Workflow ## Workflow
@@ -70,11 +65,8 @@ Load only what matches the requested artifact:
8. Keep guidance deterministic and minimal, with explicit references to source docs. 8. Keep guidance deterministic and minimal, with explicit references to source docs.
9. If artifact_type is shim: 9. If artifact_type is shim:
- bind one applyTo scope to one `skill://<name>/SKILL.md` resource URI - bind one applyTo scope to one `skill://<name>/SKILL.md` resource URI
- prefer MCP resource attachment first - use MCP resource attachment
- inspect the selected skill's `_manifest` only when supporting material is needed - inspect the selected skill's `_manifest` only when supporting material is needed
- if resource attachment is unavailable, use the generic fallback tools:
1. list_resources
2. read_resource
10. Return created or updated file paths and any validation commands that should be run. 10. Return created or updated file paths and any validation commands that should be run.
## Output Contract ## Output Contract
@@ -1,33 +1,21 @@
--- ---
name: greenfield-architecture icon: lucide/messages-square
description: Research established patterns and design a high-level architecture for a new app or library with explicit tradeoffs and test strategy. prompt:
x-personal-mcp: version: "1.0.0"
id: greenfield-architecture description: Research established patterns and design a high-level architecture for a new app or library with explicit tradeoffs and test strategy.
version: 1.0.0 tags: [architecture, planning, greenfield, design, testing, prompts]
tags:
- architecture
- planning
- greenfield
- design
- testing
- prompts
capabilities:
- resource://prompts/greenfield-architecture/document
arguments: arguments:
scope_type: scope_type:
title: Scope type description: Scope type to design.
description: "Scope type: app or library."
required: true required: true
choices: [app, library]
intent_document: intent_document:
title: Intent document description: Optional full document describing goals and context.
description: Optional full document describing goals, context, and desired outcomes.
required: false required: false
problem_domain: problem_domain:
title: Problem domain description: Problem domain and business goal.
description: Domain and business goal for the new app or library when no full intent document is provided.
required: false required: false
constraints: constraints:
title: Constraints
description: Runtime, deployment, and non-functional constraints. description: Runtime, deployment, and non-functional constraints.
required: false required: false
--- ---
@@ -36,6 +24,13 @@ x-personal-mcp:
Use this prompt to design a new software app or library architecture in generic terms. Use this prompt to design a new software app or library architecture in generic terms.
## Supplied Inputs
- `scope_type`: {{scope_type}}
- `intent_document`: {{intent_document}}
- `problem_domain`: {{problem_domain}}
- `constraints`: {{constraints}}
## Inputs ## Inputs
1. intent_document: optional full document that explains goals, context, constraints, and desired outcomes 1. intent_document: optional full document that explains goals, context, constraints, and desired outcomes
@@ -1,25 +1,27 @@
--- ---
name: jsfiddle-page-layout icon: lucide/messages-square
description: Create a responsive sample page layout for a user-supplied domain and return paste-ready HTML and CSS for JSFiddle. prompt:
x-personal-mcp: version: "1.1.0"
id: jsfiddle-page-layout description: Create a responsive sample page layout for a user-supplied domain and return paste-ready HTML and CSS for JSFiddle.
version: 1.1.0 tags: [frontend, html, css, jsfiddle, layout, prototyping, prompts]
tags: arguments:
- frontend domain:
- html description: Product, service, organization, or subject represented by the page.
- css required: true
- jsfiddle layout_brief:
- layout description: Optional page type, sections, priorities, or visual constraints.
- prototyping required: false
- prompts
capabilities:
- resource://prompts/jsfiddle-page-layout/document
--- ---
# JSFiddle Page Layout # JSFiddle Page Layout
Create a polished sample page layout for the supplied domain. The result must run by pasting the markup and styles into the [JSFiddle](https://jsfiddle.net/) HTML and CSS panes. Create a polished sample page layout for the supplied domain. The result must run by pasting the markup and styles into the [JSFiddle](https://jsfiddle.net/) HTML and CSS panes.
## Supplied Inputs
- `domain`: {{domain}}
- `layout_brief`: {{layout_brief}}
## Inputs ## Inputs
1. `domain`: the product, service, organization, or subject represented by the page, including its intended audience when known 1. `domain`: the product, service, organization, or subject represented by the page, including its intended audience when known
@@ -1,29 +1,21 @@
--- ---
name: mcp-consumer-repo-shim icon: lucide/messages-square
description: Create one repository-specific thin shim instruction file that binds a file scope to a user-selected Personal MCP skill resource and enforces resource-first Copilot retrieval behavior. prompt:
x-personal-mcp: version: "1.0.0"
id: mcp-consumer-repo-shim description: Create one repository-specific thin shim instruction file that binds a file scope to a user-selected Personal MCP skill resource.
version: 1.0.0 tags: [copilot, mcp, instructions, shims, prompts]
tags:
- copilot
- mcp
- instructions
- shims
- prompts
capabilities:
- resource://prompts/mcp-consumer-repo-shim/document
arguments: arguments:
apply_to_glob: apply_to_glob:
description: File glob scope for the shim applyTo field, such as tests/** or **/*.md. description: File glob scope for the shim applyTo field.
required: true required: true
primary_skill_resource: primary_skill_resource:
description: Primary native skill resource URI in the form skill://<skill-name>/SKILL.md. description: Primary native skill:// resource URI.
required: true required: true
shim_title: shim_title:
description: Human-readable name for the instruction shim frontmatter. description: Optional human-readable instruction shim name.
required: false required: false
companion_docs_page: companion_docs_page:
description: Optional relative docs link for human-facing companion guidance. description: Optional relative companion documentation link.
required: false required: false
--- ---
@@ -31,6 +23,13 @@ x-personal-mcp:
Use this prompt to generate exactly one repository-scoped Copilot instruction shim for an MCP consumer repository. Use this prompt to generate exactly one repository-scoped Copilot instruction shim for an MCP consumer repository.
## Supplied Inputs
- `apply_to_glob`: {{apply_to_glob}}
- `primary_skill_resource`: {{primary_skill_resource}}
- `shim_title`: {{shim_title}}
- `companion_docs_page`: {{companion_docs_page}}
## Inputs ## Inputs
- Required: - Required:
@@ -45,7 +44,7 @@ Use this prompt to generate exactly one repository-scoped Copilot instruction sh
Load only sections relevant to the requested shim: Load only sections relevant to the requested shim:
1. Thin shim pattern and scope guidance: [Skill Usage Mechanics](../../usage.md) 1. Thin shim pattern and scope guidance: [Skill Usage Mechanics](../../usage.md)
2. VS Code Copilot MCP behavior and fallback mechanics: [Copilot MCP Mechanics](../../copilot.md) 2. VS Code Copilot MCP resource behavior: [Copilot MCP Mechanics](../../copilot.md)
3. Authoring workflow and validation checklist: [Authoring Guide](../../authoring.md) 3. Authoring workflow and validation checklist: [Authoring Guide](../../authoring.md)
4. Instruction metadata expectations and examples: [Copilot customization skill](../../skills/copilot-customization/SKILL.md) 4. Instruction metadata expectations and examples: [Copilot customization skill](../../skills/copilot-customization/SKILL.md)
@@ -60,11 +59,8 @@ Load only sections relevant to the requested shim:
- include a primary rule that uses the selected primary_skill_resource first - include a primary rule that uses the selected primary_skill_resource first
- include a bounded execution pattern (load primary doc, apply only relevant sections, keep edits minimal) - include a bounded execution pattern (load primary doc, apply only relevant sections, keep edits minimal)
6. Include VS Code/Copilot integration mechanics in the shim body: 6. Include VS Code/Copilot integration mechanics in the shim body:
- prefer MCP resource attachment when available - use MCP resource attachment
- inspect `_manifest` only when the task needs supporting material - inspect `_manifest` only when the task needs supporting material
- if attachment is unavailable, use the generic fallback tools:
1. list_resources
2. read_resource
- ask one clarifying question when confidence is low - ask one clarifying question when confidence is low
7. If companion_docs_page is provided, include it as a companion docs link line. 7. If companion_docs_page is provided, include it as a companion docs link line.
8. Do not generate additional files, code changes, or batch shim packs. 8. Do not generate additional files, code changes, or batch shim packs.
@@ -98,8 +94,7 @@ Execution pattern:
3. Keep edits minimal and aligned with repository conventions. 3. Keep edits minimal and aligned with repository conventions.
4. Prefer MCP resource attachment when available in the current chat surface. 4. Prefer MCP resource attachment when available in the current chat surface.
5. Read the selected skill's `_manifest` only when supporting material is needed. 5. Read the selected skill's `_manifest` only when supporting material is needed.
6. If MCP resource attachment is unavailable, use `list_resources` and `read_resource`. 6. If confidence is low, ask one clarifying question before editing.
7. If confidence is low, ask one clarifying question before editing.
Companion docs page: <optional-relative-doc-link> Companion docs page: <optional-relative-doc-link>
``` ```
@@ -1,34 +1,21 @@
--- ---
name: nicegui-component-extraction icon: lucide/messages-square
description: Extract a user-selected component from a JSFiddle page layout and implement it as a reusable NiceGUI render function with responsive styling and typed bindable state where needed. prompt:
x-personal-mcp: version: "1.0.0"
id: nicegui-component-extraction description: Extract a user-selected component from a JSFiddle page layout and implement it as a reusable NiceGUI render function.
version: 1.0.0 tags: [nicegui, components, frontend, refactoring, jsfiddle, prompts]
tags:
- nicegui
- components
- frontend
- refactoring
- jsfiddle
- prompts
capabilities:
- resource://prompts/nicegui-component-extraction/document
arguments: arguments:
component: component:
title: Component description: Visible label, semantic role, or selector identifying the component.
description: Component or page region to extract, identified by its visible label, semantic role, or selector.
required: true required: true
source_layout: source_layout:
title: Source layout description: Optional source HTML and CSS.
description: Optional HTML and CSS from the JSFiddle page layout prompt; when omitted, use the latest applicable output in the conversation.
required: false required: false
target_location: target_location:
title: Target location description: Optional target NiceGUI page, module, or package.
description: Optional target NiceGUI page, module, or package in which to create and integrate the component.
required: false required: false
behavior_requirements: behavior_requirements:
title: Behavior requirements description: Optional interactions, state, callbacks, or variations.
description: Optional interactions, state, callbacks, or content variations the extracted component must support.
required: false required: false
--- ---
@@ -36,6 +23,13 @@ x-personal-mcp:
Extract one user-selected component from the output of the [JSFiddle Page Layout](../jsfiddle-page-layout/PROMPT.md) prompt and implement it as a reusable NiceGUI component in the target repository. Extract one user-selected component from the output of the [JSFiddle Page Layout](../jsfiddle-page-layout/PROMPT.md) prompt and implement it as a reusable NiceGUI component in the target repository.
## Supplied Inputs
- `component`: {{component}}
- `source_layout`: {{source_layout}}
- `target_location`: {{target_location}}
- `behavior_requirements`: {{behavior_requirements}}
## Inputs ## Inputs
1. `component`: required visible label, semantic role, or selector identifying the component to extract 1. `component`: required visible label, semantic role, or selector identifying the component to extract
@@ -47,10 +41,11 @@ If the selected component or source layout cannot be identified unambiguously, a
## Required References ## Required References
Apply both references before implementation: Apply these references before implementation:
1. Component boundaries, responsive layout, Quasar props, Tailwind utilities, and shared CSS: [NiceGUI Page Layout and Styling](../../skills/nicegui/references/architecture-and-styling.md) 1. Package boundaries, dependency direction, and page or component ownership: [NiceGUI Application Architecture](../../skills/nicegui/references/architecture.md)
2. Typed UI state, propagation, mutable defaults, binding strictness, and version checks: [Binding Dataclasses Deep Dive](../../skills/nicegui/references/binding-dataclasses.md) 2. Responsive layout, Quasar props, Tailwind utilities, and shared CSS: [NiceGUI Styling and Customization](../../skills/nicegui/references/styling-and-customization.md)
3. Typed UI state, propagation, mutable defaults, binding strictness, and version checks: [Binding Dataclasses Deep Dive](../../skills/nicegui/references/binding-dataclasses.md)
## Workflow ## Workflow
@@ -1,16 +1,9 @@
--- ---
name: pytest-fill-scaffold icon: lucide/messages-square
description: Fill scaffolded pytest test methods with assertions, fixtures, and minimal test data while preserving concise test names and one-line intent docstrings. prompt:
x-personal-mcp: version: "1.0.0"
id: pytest-fill-scaffold description: Fill scaffolded pytest methods with assertions, fixtures, and minimal test data while preserving reviewed structure.
version: 1.0.0 tags: [pytest, testing, scaffolding, prompts]
tags:
- pytest
- testing
- scaffolding
- prompts
capabilities:
- resource://prompts/pytest-fill-scaffold/document
arguments: arguments:
target_files: target_files:
description: Target test file paths under tests/. description: Target test file paths under tests/.
@@ -18,11 +11,12 @@ x-personal-mcp:
stack: stack:
description: Runtime stack type for fixture and marker choices. description: Runtime stack type for fixture and marker choices.
required: true required: true
choices: [pure-python, fastapi, sqlalchemy-sync, sqlalchemy-async, mixed]
strategy: strategy:
description: Balance between minimal and comprehensive implementation. description: Optional minimal or comprehensive implementation preference.
required: false required: false
marker_lane: marker_lane:
description: Preferred marker lane when applicable. description: Optional pytest marker lane.
required: false required: false
--- ---
@@ -30,6 +24,13 @@ x-personal-mcp:
Use this prompt after test scaffolding exists and method names/docstrings are already in place. Use this prompt after test scaffolding exists and method names/docstrings are already in place.
## Supplied Inputs
- `target_files`: {{target_files}}
- `stack`: {{stack}}
- `strategy`: {{strategy}}
- `marker_lane`: {{marker_lane}}
## Inputs ## Inputs
- Target test file(s) under tests/. - Target test file(s) under tests/.
@@ -1,28 +1,22 @@
--- ---
name: pytest-scaffold icon: lucide/messages-square
description: Plan and optionally scaffold pytest file and class structure for selected Python modules while preserving concise behavior-focused test names and one-line intent docstrings. prompt:
x-personal-mcp: version: "1.0.0"
id: pytest-scaffold description: Plan and optionally scaffold pytest file and class structure for selected Python modules.
version: 1.0.0 tags: [pytest, testing, scaffolding, prompts]
tags:
- pytest
- testing
- scaffolding
- prompts
capabilities:
- resource://prompts/pytest-scaffold/document
arguments: arguments:
target_modules: target_modules:
description: Target module path(s) under src/. description: Target module paths under src/.
required: true required: true
mode: mode:
description: Execution mode, either plan-only or scaffold. description: Whether to plan only or create scaffold files.
required: true required: true
choices: [plan-only, scaffold]
path_strategy: path_strategy:
description: Optional mapping preference for src to tests paths. description: Optional src-to-tests path mapping preference.
required: false required: false
naming_style: naming_style:
description: Optional preference for concise method naming style. description: Optional concise test naming preference.
required: false required: false
--- ---
@@ -30,6 +24,13 @@ x-personal-mcp:
Use this prompt to consistently plan and scaffold pytest test modules for selected Python source modules. Use this prompt to consistently plan and scaffold pytest test modules for selected Python source modules.
## Supplied Inputs
- `target_modules`: {{target_modules}}
- `mode`: {{mode}}
- `path_strategy`: {{path_strategy}}
- `naming_style`: {{naming_style}}
## Inputs ## Inputs
- Required: - Required:
+42
View File
@@ -0,0 +1,42 @@
---
icon: lucide/shield-check
---
# Security
## Public By Design
The application does not implement authentication. Its current purpose is to publish read-only guidance, so everything exposed through the server must be safe to make public.
This includes:
- the website under `/docs/`
- resources and prompts under `/mcp`
- the four read-only fallback tools
Do not add secrets, private notes, credentials, or sensitive environment details to the authored content.
## Remote Deployment
Place the service behind a reverse proxy or tunnel rather than exposing the container directly. The edge can provide TLS, rate limiting, access logs, and optional authentication without adding those concerns to this small application.
A simple deployment is:
```text
Internet -> reverse proxy or tunnel -> personal-mcp
```
The website and MCP endpoint can remain public while they contain only public, read-only content. Use edge authentication if access should be limited.
## When Authentication Becomes Required
Protect `/mcp` before adding any capability that can:
1. read non-public files
2. access private data or credentials
3. call authenticated services
4. mutate data
5. run commands
6. perform expensive work
At that point, choose authentication based on the clients that need to connect. Edge authentication is the simplest option for a small trusted audience; standards-based MCP authorization is more appropriate when broad client interoperability is required.
@@ -0,0 +1,239 @@
---
name: cli-client
description: 'Design, implement, review, or refine Python command-line clients for remote APIs. Use for command structure, automation-friendly input and output, configuration, authentication, HTTP transport, pagination, retries, errors and exit codes, state-changing operations, packaging, or CLI testing.'
---
# Python API CLI Clients
Use this skill as a conceptual reference for command-line applications that operate remote services. Preserve established project conventions, but make the command surface predictable for people, scripts, CI jobs, and shell composition.
## Design Priorities
A good API CLI should be:
- **Task-oriented:** commands reflect user goals rather than HTTP endpoints or internal service classes.
- **Predictable:** names, flags, defaults, output, errors, and exit statuses behave consistently.
- **Composable:** successful data goes to stdout, diagnostics go to stderr, and machine-readable output is stable.
- **Safe:** destructive operations are explicit, retries respect operation semantics, and secrets never enter output or logs.
- **Layered:** command parsing, application behavior, API resources, transport, authentication, and persistence have distinct owners.
- **Inspectable:** users can discover commands and effective configuration without reading source code.
Use the [Command Line Interface Guidelines](https://clig.dev/) as the general human-interface baseline. Prefer the target project's established command framework and HTTP library over introducing replacements without a concrete need.
## Command Model
Design the command tree around a small, consistent grammar:
```text
mycli <resource> <action> [arguments] [options]
mycli projects list --owner alice
mycli projects get PROJECT_ID
mycli projects create --name NAME
mycli projects delete PROJECT_ID
```
- Use nouns for resource groups and familiar verbs for actions.
- Keep equivalent operations parallel across resources: `list`, `get`, `create`, `update`, and `delete` should not change meaning by command group.
- Prefer explicit positional arguments for primary identities and named options for modifiers.
- Reserve global options for behavior that applies consistently across commands, such as profile, endpoint, output format, verbosity, and non-interactive mode.
- Give every command useful `--help` output with a one-line purpose, argument meaning, defaults, and behavior-changing caveats.
- Avoid mirroring every server endpoint. Combine low-level calls when one user task requires them, and omit endpoints that do not form a coherent CLI operation.
Do not make users memorize hidden context. When a command depends on an active account, project, region, or profile, make that context discoverable and overridable.
## Responsibility Boundaries
Keep the command layer thin and dependencies directional:
```text
entry point and bootstrap
└── command groups
└── application services
└── API client and resources
└── authenticated HTTP transport
└── HTTP library
configuration ───────────────┘
credentials ──> authentication
renderers <──── command results
```
| Layer | Owns | Avoid |
| --- | --- | --- |
| Entry point | Dependency construction, top-level exception mapping, process exit | Business logic and API calls |
| Command | Parsing, prompts, presentation, command-specific orchestration | Raw HTTP details and credential refresh |
| Application service | Multi-request use cases and domain decisions | Terminal formatting |
| API resource | Endpoint paths, request parameters, and response models | CLI prompts and global process state |
| Transport | Base URL, headers, serialization, timeouts, retries, and response decoding | Resource-specific business rules |
| Authentication | Credential acquisition, storage, and renewal | API resource behavior |
| Renderer | Human and machine-readable output | Network calls and state mutation |
Keep framework objects at the command boundary. Core operations should accept ordinary typed values and return structured results so they can be tested without invoking a subprocess.
## Input And Interaction
- Accept flags for every value that automation may need to provide. Prompts are an interactive convenience, not the only input path.
- Prompt only when stdin and stderr are attached to a terminal and the user has not selected non-interactive mode.
- In non-interactive mode, fail quickly with a specific missing-input error instead of waiting for input.
- Read large request bodies from a file or stdin; avoid forcing structured documents into shell-escaped arguments.
- Distinguish an omitted option from an explicit empty value when the API supports partial updates.
- Validate syntax locally, but let the service remain authoritative for remote identities, permissions, and business rules.
- Support `--` before pass-through values or positional arguments that can begin with a hyphen.
For destructive or difficult-to-reverse operations, state the target precisely and require confirmation in interactive sessions. Provide an explicit option such as `--yes` for automation; never silently infer consent merely because input is non-interactive.
## Output Contract
Treat output as a public interface.
- Write requested results to stdout and diagnostics, progress, warnings, and errors to stderr.
- Make the default human output concise and scannable. Do not print the same result as both prose and a table.
- Provide one stable machine-readable format, usually `--output json` or `--json`, for commands whose results are useful in automation.
- Serialize machine output from typed result models rather than scraping human-formatted strings.
- Keep machine-readable stdout clean: no progress bars, update notices, color codes, or explanatory prefixes.
- Disable color and animated progress when the output stream is not a terminal or when the user requests it.
- Use a pager only for interactive human output, and provide a consistent way to disable it.
- Document whether list commands emit one aggregate value or a stream of records; do not switch shapes based on result count.
When adding fields, preserve existing machine-readable fields where practical. Treat renaming, removing, or changing the type of a field as a compatibility decision.
## Configuration Model
Use one documented precedence order:
```text
command-line option > environment variable > selected profile/config file > built-in default
```
- Resolve configuration once near startup and pass a validated settings object inward.
- Keep endpoint, profile, timeout, output mode, and similar behavior visible through a config or diagnostics command.
- Show provenance when troubleshooting precedence, but redact secret values.
- Store configuration in platform-appropriate user directories rather than the current working directory unless project-local configuration is intentional.
- Keep credentials behind a separate storage abstraction. A convenient config file is not automatically an acceptable secret store.
- Validate incompatible options together and report the conflict in the user's vocabulary.
## HTTP And API Behavior
Centralize remote-call behavior in the transport or API client:
- Set explicit connect, read, write, and pool timeouts appropriate to the service.
- Send a useful user agent containing the CLI name and version.
- Map service errors into a small application error taxonomy before they reach commands.
- Retry only transient failures, honor `Retry-After`, cap attempts and elapsed time, and add jitter where concurrent clients may synchronize.
- Automatically retry state-changing requests only when they are demonstrably replay-safe, such as through an idempotency key accepted by the service.
- Preserve server request or correlation IDs in verbose diagnostics without exposing sensitive response data.
- Keep pagination in the API layer. Let commands choose whether to fetch one page, stream pages, or collect all results based on output and memory requirements.
- Make cancellation responsive between requests and during long-running operations.
Do not leak raw HTTP-library exceptions as the normal user interface. Preserve the original exception as the cause for debugging while presenting a stable CLI-level error.
## Authentication
Choose authentication from the service contract and execution context. Keep credential acquisition and renewal out of command handlers and API resources.
For OAuth-protected APIs, load [OAuth 2.0 for installed CLI clients](./references/oauth.md). It covers public clients, Authorization Code with PKCE, loopback callbacks, device authorization, Authlib and HTTPX2 boundaries, protected token storage, synchronized refresh, scopes, discovery, and bounded authentication retries.
For API keys or static tokens:
- Accept them through an explicit credential provider such as an OS credential store, environment variable, or CI secret integration.
- Define precedence when more than one provider is configured.
- Never place credentials in command arguments by default because process listings and shell history may expose them.
- Redact credentials and credential-like headers from errors, debug logs, traces, and support bundles.
For unattended workloads, use a service identity and grant intended for machines. Do not reuse a person's interactive credentials as automation identity.
## Errors And Exit Status
Keep a small documented taxonomy and map it once at the entry point:
| Category | User-facing behavior | Exit-status requirement |
| --- | --- | --- |
| Usage or validation | Explain the invalid input and show the nearest help hint | Stable nonzero status distinct from remote failure |
| Authentication | Explain whether login or credential repair is required | Stable nonzero status |
| Authorization | Identify the denied operation without claiming credentials are expired | Stable nonzero status |
| Not found or conflict | Name the target and preserve actionable server context | Stable nonzero status if scripts branch on it |
| Rate limit or transient service failure | Explain retryability and any known retry time | Stable nonzero status |
| Unexpected failure | Concise message plus opt-in diagnostic detail | Generic nonzero status |
- Return zero only when the requested operation completed according to its contract.
- Do not require scripts to parse prose to distinguish common failure categories.
- Keep normal errors concise. Put tracebacks, request details, and internal context behind an explicit debug or verbose mode.
- Handle interruption without a traceback by default and use the platform's conventional interrupted-process status.
- Preserve partial-success information for batch operations and define whether partial success is a failing exit status.
## State-Changing Operations
- Display or return the identity of the affected resource.
- Support a dry-run or plan mode when the service can accurately predict a consequential change.
- Use idempotency keys for retried creates or actions when the API supports them.
- Do not claim rollback if the remote API cannot provide it.
- For batch changes, define ordering, concurrency limits, stop/continue behavior, and partial-failure reporting.
- Keep local caches disposable unless their contents are explicitly part of the user contract.
## Concurrency And Async Boundaries
Use concurrency only where it improves a measured workflow such as independent page or resource retrieval. Bound concurrent requests to respect service and local limits.
Choose one owner for the event loop. Command handlers may call an async application boundary, but lower layers should not invoke nested event-loop runners. Keep synchronous and asynchronous APIs separate or adapt them in one explicit place.
## Suggested Package Shape
Adapt this shape to the project's size and existing conventions:
```text
src/mycli/
├── __main__.py
├── cli.py
├── config.py
├── errors.py
├── output.py
├── api/
│ ├── client.py
│ ├── transport.py
│ └── resources/
└── auth/
```
Small clients can combine modules while preserving the conceptual boundaries. Split code when a boundary has distinct dependencies, state, tests, or change cadence, not merely to reproduce the example tree.
## Design Sequence
1. Inventory the user tasks, execution environments, API capabilities, and existing project conventions.
2. Define the command grammar, required inputs, destructive-operation policy, output modes, and exit-status contract before wiring endpoints.
3. Define typed configuration and its precedence, including credential providers and active context.
4. Establish API resource, transport, authentication, and error boundaries.
5. Implement one vertical command path through parsing, service behavior, transport, rendering, and error mapping.
6. Verify the path both in-process and as an installed subprocess before repeating the pattern.
7. Add concurrency, retries, caching, rich presentation, and convenience prompts only where requirements justify them.
## Testing Strategy
- Unit-test command-independent services, API resources, renderers, configuration resolution, and error mapping directly.
- Test command invocation with isolated environment variables, config directories, stdin, stdout, and stderr.
- Assert exit status, stdout, and stderr independently.
- Cover human and machine-readable output, including empty and multi-page results.
- Use a mock transport for timeouts, malformed responses, pagination, rate limits, transient retries, and permanent failures.
- Verify interactive confirmation and non-interactive refusal for destructive operations.
- Test redaction with realistic secret shapes in headers, URLs, response bodies, and nested exceptions.
- Add live-service tests only for behavior a local fake cannot represent, using isolated accounts and CI-managed credentials.
- Build and install the distribution in a clean environment to verify the console entry point and runtime dependencies.
## Reference Map
| Topic | Load when | Reference |
| --- | --- | --- |
| Python library selection | Choosing or comparing a parser framework, terminal output, TUI, configuration, HTTP, authentication, testing, or packaging stack | [Python CLI library selection](./references/library-selection.md) |
| OAuth for installed applications | The API uses OAuth, OIDC discovery, refresh tokens, loopback callbacks, or device authorization | [OAuth 2.0 for installed CLI clients](./references/oauth.md) |
## Completion Checks
1. Commands model recognizable user tasks with consistent names and options.
2. Interactive conveniences have explicit non-interactive equivalents.
3. Stdout, stderr, machine output, and exit statuses form a stable automation contract.
4. Configuration has one visible precedence order and credentials use an appropriate protected source.
5. Commands do not own raw HTTP, authentication lifecycle, or terminal-independent business logic.
6. Timeouts, retries, pagination, cancellation, and state-changing request safety are explicit.
7. Errors are actionable, categorized, redacted, and mapped at one process boundary.
8. Destructive and batch operations define confirmation, idempotency, and partial-failure behavior.
9. Tests exercise installed command behavior as well as isolated application and transport logic.
10. Help text and diagnostics let users discover the command surface and effective non-secret configuration.
@@ -0,0 +1,175 @@
# Python CLI Library Selection
Use this reference when choosing libraries for a new Python CLI or deciding whether an existing stack still fits. Choose each layer independently: a command parser, terminal renderer, terminal UI, settings model, HTTP client, authentication implementation, test runner, and package manager solve different problems.
## Default Stack
For a new typed application CLI, start with:
| Concern | Default | Why |
| --- | --- | --- |
| Command parsing | [Cyclopts](https://cyclopts.readthedocs.io/en/latest/) | Type-driven commands, rich type support, docstring-derived help, validation, command groups, configuration sources, and testing helpers |
| Human terminal output | [Rich](https://rich.readthedocs.io/en/stable/) | Tables, progress, status, syntax, terminal detection, and separate output consoles |
| Settings and data validation | [Pydantic](https://docs.pydantic.dev/latest/) and [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) | Typed models and explicit environment, dotenv, secret, and custom settings sources |
| HTTP | [HTTPX2](https://pydantic.dev/docs/httpx2/) | Actively maintained synchronous and asynchronous APIs, explicit clients, timeouts, streaming, and testable transports |
| OAuth | [Authlib 1.8+](https://pypi.org/project/Authlib/) | OAuth implementation integrated with HTTPX2; use the [OAuth reference](./oauth.md) for architecture and security requirements |
| Tests | [pytest](https://docs.pytest.org/en/stable/) | Fixtures, parametrization, output capture, monkeypatching, and a broad plugin ecosystem |
| Project and environment | [uv](https://docs.astral.sh/uv/) | Project dependencies, lockfiles, environments, scripts, builds, and tool execution |
Add [Textual](https://textual.textualize.io/) only when the product needs a persistent, event-driven terminal interface. It complements a command parser; it does not replace the scriptable command surface.
This is a starting point, not a mandate. Preserve a sound existing stack unless a requirement exposes a concrete limitation.
## Choose The Command Framework
### Use Cyclopts by default for a new typed application CLI
[Cyclopts](https://cyclopts.readthedocs.io/en/latest/) derives commands and parameters from Python function signatures and supports built-in and user-defined types, including unions, literals, dataclasses, Pydantic models, and attrs classes. It can derive help from docstrings and provides converters, validators, nested commands, lazy loading, configuration sources, documentation integration, and testing guidance.
Choose Cyclopts when:
- Modern type annotations should be the primary command schema.
- Commands accept structured dataclasses or validation models.
- Rich unions, literals, nested structures, or reusable parameter groups matter.
- The project values concise declarations and generated documentation.
- A newer and smaller ecosystem is an acceptable tradeoff.
Before committing, prototype the hardest command signature, help page, validation error, completion behavior, and test invocation. Do not evaluate a framework only on a one-command example.
### Use Typer for the mainstream type-driven choice
[Typer](https://typer.tiangolo.com/) also derives CLI arguments and options from Python type hints and provides automatic help, shell completion, nested command groups, Rich-formatted output, packaging guidance, and test helpers. It is a strong choice when contributor familiarity, established examples, and ecosystem recognition matter more than Cyclopts' broader type model.
Since Typer 0.26.0, [Typer vendors Click](https://typer.tiangolo.com/#click-code) rather than depending on the external Click package. Do not assume an arbitrary Click extension or subclass will integrate with modern Typer; verify that requirement against the installed Typer release.
Choose Typer when:
- The team already knows Typer or follows the FastAPI ecosystem.
- The command types fit Typer's supported parameter model.
- A familiar, established type-driven framework lowers contributor cost.
- Existing Typer conventions or integrations outweigh framework-switching benefits.
### Use Click when explicit control is the requirement
[Click](https://click.palletsprojects.com/en/stable/) models commands, groups, contexts, parameters, types, and invocation explicitly. It supports arbitrary command nesting, lazy subcommand loading, custom parameter types, extension APIs, testing utilities, and a mature plugin ecosystem.
Choose Click when:
- The CLI is itself a framework or plugin host.
- Commands must be discovered or loaded lazily.
- Parsing, context propagation, invocation, or help behavior needs unusual customization.
- Existing Click extensions are a hard dependency.
- Explicit declarations are preferable to inference from application types.
Do not choose Click merely because it is mature. For ordinary application commands, the extra parser-level detail may duplicate function types, defaults, validation, and documentation.
### Use argparse when dependency constraints dominate
[`argparse`](https://docs.python.org/3/library/argparse.html) is the standard-library parser and supports subcommands, generated help, custom actions and types, argument files, and parser-level error handling. Python 3.14 added colored help and `suggest_on_error`, making its default experience more capable than older comparisons imply.
Choose argparse when:
- The tool must remain standard-library-only.
- It is a small utility with a stable and modest command surface.
- Conservative deployment environments value availability over declaration ergonomics.
- Adding a runtime dependency has a real operational cost.
For a substantial typed application, account for the duplication between parser declarations and the application's function signatures, models, defaults, validation, and help text.
### Use Fire for exposure, not deliberate public design
[Python Fire](https://github.com/google/python-fire) generates a CLI from functions, classes, modules, mappings, and other Python objects. This is useful for developer tools, debugging, exploration, and rapidly exposing an internal Python API.
Avoid Fire as the default for a stable public CLI. Exposing the Python object model couples command names, arguments, and behavior to implementation details instead of treating the CLI as a deliberately designed compatibility surface.
## Framework Decision Table
| Primary requirement | Choose | Main tradeoff |
| --- | --- | --- |
| New, typed application with rich parameter models | Cyclopts | Smaller ecosystem and less accumulated operational history |
| Type-driven CLI with maximum contributor familiarity | Typer | Verify complex typing and Click-extension assumptions |
| Plugin framework or unusual parser behavior | Click | More explicit declarations and parser-specific code |
| Standard-library-only or tiny utility | argparse | Imperative setup and duplicated schema information |
| Rapid internal exposure of Python objects | Fire | Python implementation becomes the CLI contract |
When Cyclopts and Typer both fit, build the same representative vertical slice in each. Include the most complex parameter model, nested command registration, configuration injection, help output, validation failure, shell completion, and command test. Select from that evidence rather than syntax preference.
## Keep Complementary Libraries In Their Layer
### Rich is presentation, not parsing
Use [Rich](https://rich.readthedocs.io/en/stable/) behind a renderer abstraction for human-readable tables, progress, status displays, syntax, and styled errors. Keep structured output on a separate serialization path so `--output json` never contains decoration, progress, or terminal control codes.
Typer includes Rich as a dependency and uses it for formatted errors. Cyclopts can also produce Rich-oriented help and errors. This does not remove the need for an application-owned output boundary.
### Textual is an optional interactive mode
Use [Textual](https://textual.textualize.io/) when users need a persistent screen, navigation, reactive widgets, keyboard actions, or live dashboards. Keep ordinary parser commands for automation and direct operations:
```text
mycli projects list -> scriptable command
mycli interactive -> Textual application
both -> application services -> API client
```
The command and TUI adapters should call the same application services. Do not embed API and domain behavior separately in Textual event handlers.
## Supporting Stack Decisions
### Configuration and models
Use dataclasses when configuration is small, already parsed, and needs no source orchestration. Use [Pydantic](https://docs.pydantic.dev/latest/) for structured request, response, configuration, or command models that benefit from validation and serialization. Use [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) when environment variables, dotenv files, secret files, or custom settings sources participate in explicit precedence.
Do not pass framework parameter objects into application services. Convert parser output into ordinary typed values or application models at the command boundary.
### HTTP
Use [HTTPX2](https://pydantic.dev/docs/httpx2/) for new API clients. It is the actively developed continuation of HTTPX and supports synchronous and asynchronous clients, explicit timeouts, streaming, custom authentication, and mock transports. [Authlib 1.8+](https://pypi.org/project/Authlib/) integrates with HTTPX2 directly. Reuse a client with explicit timeouts rather than calling top-level request functions throughout resource methods.
Preserve [HTTPX](https://www.python-httpx.org/) in a sound existing client until its dependencies, type checks, and transport tests are ready to migrate. Do not use HTTPX2's process-wide import alias from reusable library code, and do not keep HTTPX and HTTPX2 as permanent parallel transports without a concrete compatibility requirement.
Use [aiohttp](https://docs.aiohttp.org/en/stable/) when the project already standardizes on its async client, depends on its streaming or WebSocket behavior, or has measured requirements that justify a different transport. Do not introduce both HTTPX2 and aiohttp without a clear ownership boundary.
### Authentication
Use [Authlib 1.8+](https://pypi.org/project/Authlib/) for OAuth protocol behavior rather than implementing grants, PKCE, token parsing, and refresh directly. Keep it behind an authentication abstraction. For OAuth-enabled installed applications, follow [OAuth 2.0 for installed CLI clients](./oauth.md).
Use [keyring](https://keyring.readthedocs.io/en/latest/) to access operating-system credential stores when the deployment environment provides one. Treat headless secret storage as an explicit deployment decision rather than silently falling back to plaintext configuration.
### Testing and packaging
Use [pytest](https://docs.pytest.org/en/stable/) for application and command tests. Combine framework-level invocation helpers with subprocess tests of the installed console entry point; a runner helper alone does not verify packaging or startup behavior.
Use [uv](https://docs.astral.sh/uv/) for dependency management, lockfiles, isolated tool execution, and project commands when the repository adopts uv. Declare the CLI through a `[project.scripts]` entry point in [`pyproject.toml`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#creating-and-packaging-command-line-tools) so installation, tests, and users invoke the same bootstrap path.
## Architecture Rule
Do not couple application behavior to the chosen CLI framework:
```text
Cyclopts / Typer / Click / argparse
|
v
thin command adapters
|
v
application services
|
v
API client
```
Command functions should parse or receive values, call an application service, and hand the result to a renderer. Keep API calls, authentication, retries, and domain decisions outside parser decorators and callbacks. This makes framework-specific tests small and keeps a future parser migration bounded.
## Selection Checklist
1. Identify the minimum supported Python version and dependency constraints.
2. Model the hardest real command, not the smallest demonstration command.
3. Decide whether type annotations or explicit parser objects should own the command schema.
4. Check complex types, nested commands, lazy loading, plugins, completion, help, validation, and test support against actual requirements.
5. Separate parsing from Rich presentation and optional Textual interaction.
6. Select configuration, HTTP, authentication, testing, and packaging libraries independently.
7. Prototype ambiguous framework choices with the same vertical slice.
8. Pin compatible versions and verify behavior against installed-library documentation before implementation.
9. Keep application services free of CLI-framework types.
10. Preserve stable stdout, stderr, and exit-status contracts regardless of library defaults.
@@ -0,0 +1,513 @@
# OAuth 2.0 For Installed CLI Clients
Use this reference when a Python CLI calls an OAuth-protected API. Keep authentication, token lifecycle, HTTP transport, and API resources as separate ownership boundaries.
## Recommended Default
For an interactive installed CLI, use:
> Public client + Authorization Code + PKCE using `S256` + loopback callback + Authlib 1.8+ + HTTPX2 + protected credential store + centralized token refresh + minimum scopes.
Follow the current [OAuth 2.0 Security Best Current Practice](https://www.rfc-editor.org/rfc/rfc9700) and the [OAuth 2.0 guidance for native applications](https://www.rfc-editor.org/rfc/rfc8252). Do not use the implicit grant, Resource Owner Password Credentials grant, or an embedded client secret.
## Responsibility Boundaries
Keep dependencies pointed inward toward authentication and transport primitives:
```text
CLI commands
├── login/logout/status -> OAuthManager -> authorization server
└── API commands -> ApiClient/resources -> OAuthTransport
└── TokenManager
├── TokenStore
└── OAuth client / HTTP transport
```
| Component | Owns | Must not own |
| --- | --- | --- |
| `OAuthManager` | Interactive login, callback validation, token exchange, logout | API resource methods |
| `TokenStore` | Loading, atomically saving, and deleting sensitive token state | Refresh policy or HTTP requests |
| `TokenManager` | Expiry checks, refresh synchronization, and valid access-token retrieval | CLI presentation or resource URLs |
| `OAuthTransport` | Bearer-token injection and one bounded authentication retry | Interactive login UX |
| `ApiClient` and resources | API operations, request models, and response models | OAuth grants, refresh tokens, or credential storage |
Keep [Authlib's HTTPX2 OAuth client](https://github.com/authlib/authlib/blob/v1.8.0/authlib/integrations/httpx_client/oauth2_client.py) behind the authentication boundary. API resources should request an authenticated transport or call `TokenManager.get_valid_access_token()`; they should not depend directly on Authlib.
## Library Selection
Use one library per responsibility and keep each one behind an application-owned interface:
| Concern | Default for new code | Use something else when |
| --- | --- | --- |
| OAuth protocol | [Authlib 1.8+](https://pypi.org/project/Authlib/) | A provider supplies an official, maintained SDK that correctly implements its non-standard behavior |
| API and OAuth HTTP | [HTTPX2 2.x](https://pydantic.dev/docs/httpx2/) | Preserve HTTPX in an existing stable client until its dependencies and test doubles are ready to migrate |
| Desktop credential storage | [keyring](https://keyring.readthedocs.io/en/latest/) | The target platforms are explicitly supported by a reviewed native alternative, or the deployment already owns a managed vault |
| Async coordination | [`asyncio`](https://docs.python.org/3/library/asyncio-sync.html) for an asyncio-only CLI | The application already standardizes on [AnyIO](https://anyio.readthedocs.io/en/stable/) or supports multiple async backends |
| Tests | [pytest](https://docs.pytest.org/en/stable/) and [`httpx2.MockTransport`](https://pydantic.dev/docs/httpx2/advanced/transports/#mock-transports) | The repository has an established equivalent |
[HTTPX2](https://pypi.org/project/httpx2/) is the actively developed continuation of HTTPX under Pydantic stewardship. It keeps the familiar client, request, response, authentication, and mock-transport APIs while using the `httpx2` import. Authlib 1.8 moved its HTTP client integration to HTTPX2, so new OAuth clients can use one HTTP implementation:
```bash
uv add "Authlib>=1.8" "httpx2>=2" keyring
```
Do not call [`httpx2.alias_httpx()`](https://pydantic.dev/docs/httpx2/api/api/#httpx2.alias_httpx) from reusable library code. It changes imports process-wide and exists as a temporary application-level migration aid. If an existing dependency still requires `httpx`, keep both clients behind local abstractions and remove HTTPX only after dependency and transport tests pass.
## Configuration And Secret State
Separate public OAuth configuration from sensitive OAuth state.
Public configuration may contain:
- Client ID for the registered public client.
- Issuer, authorization endpoint, token endpoint, and optional revocation or device-authorization endpoint.
- Exact registered redirect URI rules.
- Required scopes and, when supported, resource or audience indicators.
Sensitive state includes:
- Access tokens.
- Refresh tokens.
- Token expiry and related token response fields when they reveal account or authorization state.
Represent the complete token response and store it through a narrow abstraction. A `TypedDict` documents the common fields without discarding provider-specific fields from the runtime dictionary:
```python
from typing import Protocol, Required, TypedDict
class OAuthToken(TypedDict, total=False):
access_token: Required[str]
refresh_token: str
token_type: str
expires_at: float
expires_in: int
scope: str
class TokenStore(Protocol):
async def load(self) -> OAuthToken | None: ...
async def save(self, token: OAuthToken) -> None: ...
async def delete(self) -> None: ...
```
For a desktop CLI, [keyring](https://keyring.readthedocs.io/en/latest/) remains the conservative default because it selects macOS Keychain, Windows Credential Locker, Secret Service, or KWallet as available. Its API is synchronous, so move calls off the event-loop thread. Store one JSON document per account so access-token and refresh-token rotation is replaced as one logical update:
```python
import json
from typing import cast
import anyio
import keyring
from keyring.backend import KeyringBackend
from keyring.errors import KeyringError
class CredentialStoreError(RuntimeError):
pass
class KeyringTokenStore:
def __init__(
self,
service: str,
account: str,
backend: KeyringBackend | None = None,
) -> None:
self._service = service
self._account = account
self._backend = backend or keyring.get_keyring()
if self._backend.priority <= 0:
raise CredentialStoreError("No protected credential store is available")
async def load(self) -> OAuthToken | None:
try:
raw = await anyio.to_thread.run_sync(
self._backend.get_password, self._service, self._account
)
except KeyringError as error:
raise CredentialStoreError("Could not read OAuth credentials") from error
if raw is None:
return None
try:
value = json.loads(raw)
except json.JSONDecodeError as error:
raise CredentialStoreError("Stored OAuth credentials are invalid") from error
if not isinstance(value, dict) or not isinstance(value.get("access_token"), str):
raise CredentialStoreError("Stored OAuth credentials are invalid")
return cast("OAuthToken", value)
async def save(self, token: OAuthToken) -> None:
encoded = json.dumps(token, separators=(",", ":"))
try:
await anyio.to_thread.run_sync(
self._backend.set_password,
self._service,
self._account,
encoded,
)
except KeyringError as error:
raise CredentialStoreError("Could not save OAuth credentials") from error
async def delete(self) -> None:
if await self.load() is None:
return
try:
await anyio.to_thread.run_sync(
self._backend.delete_password, self._service, self._account
)
except KeyringError as error:
raise CredentialStoreError("Could not delete OAuth credentials") from error
```
Fail closed when no recommended backend is available. Do not install [`keyrings.alt`](https://pypi.org/project/keyrings.alt/) as an automatic fallback; it intentionally includes possibly insecure backends.
### Storage Alternatives
- Consider [`rust-native-keyring`](https://pypi.org/project/rust-native-keyring/) when native compiled wheels, the Rust [keyring ecosystem](https://github.com/open-source-cooperative/keyring-rs), and its richer credential-store selection fit the supported platforms. Its Python package is still `0.x`, so pin it, verify wheel availability, and test lock/unlock and deletion behavior on every target OS before preferring it over `keyring`.
- Consider the official [1Password Python SDK](https://www.1password.dev/sdks/) when users already rely on 1Password and desktop authorization prompts, auditing, or shared vault policy are product requirements. It is asynchronous and supports desktop-app authorization, but its SDK is also still version `0`; it is not a transparent local-keyring replacement.
- Use a managed service such as [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets-python-sdk.html), [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-python), [Google Secret Manager](https://docs.cloud.google.com/secret-manager/docs/reference/libraries), or [HashiCorp Vault](https://developer.hashicorp.com/vault/docs/get-started/developer-qs) for unattended or centrally governed deployments. Authenticate with workload identity or another deployment-owned mechanism; do not solve storage by introducing a second long-lived bootstrap secret.
For a headless environment without a credential service, require an explicit storage backend appropriate to its threat model; do not silently fall back to plaintext config. Never emit tokens through logs, telemetry, exceptions, shell output, or status commands.
## Interactive Authorization
Use Authorization Code with PKCE for browser-based login:
1. Generate a cryptographically random `state` and a fresh PKCE `code_verifier` for every attempt.
2. Derive the `S256` code challenge and construct the authorization URL with the exact requested redirect URI and minimum scopes.
3. Bind a temporary listener to `127.0.0.1` on an ephemeral port. Do not expose it on all interfaces.
4. Open the system browser and wait for one callback with a short timeout and cancellation path.
5. Reject OAuth errors, a missing code, or any callback whose `state` does not exactly match.
6. Exchange the code using the original verifier and redirect URI.
7. Persist the complete returned token state atomically, then stop the listener immediately.
8. Return a minimal success page and CLI message without displaying credentials.
Treat the CLI as a public client. A secret distributed inside source, a package, a binary, or an environment-independent configuration file cannot authenticate installed copies of the CLI.
The following manager shows the Authlib-owned part of a loopback flow. A separate callback receiver should bind `127.0.0.1`, accept one request, enforce a timeout and maximum request size, then pass the complete callback URL to `finish()`:
```python
import secrets
from dataclasses import dataclass
from urllib.parse import parse_qs, urlsplit
from authlib.integrations.httpx_client import AsyncOAuth2Client
@dataclass(frozen=True, slots=True)
class OAuthConfig:
client_id: str
authorization_endpoint: str
token_endpoint: str
redirect_uri: str
scopes: tuple[str, ...]
@dataclass(frozen=True, slots=True)
class PendingAuthorization:
url: str
state: str
code_verifier: str
class OAuthManager:
def __init__(self, config: OAuthConfig, store: TokenStore) -> None:
self._config = config
self._store = store
self._client = AsyncOAuth2Client(
client_id=config.client_id,
redirect_uri=config.redirect_uri,
scope=" ".join(config.scopes),
code_challenge_method="S256",
token_endpoint_auth_method="none",
)
def begin(self) -> PendingAuthorization:
verifier = secrets.token_urlsafe(64)
url, state = self._client.create_authorization_url(
self._config.authorization_endpoint,
code_verifier=verifier,
)
return PendingAuthorization(url=url, state=state, code_verifier=verifier)
async def finish(
self, callback_url: str, pending: PendingAuthorization
) -> OAuthToken:
callback = urlsplit(callback_url)
expected = urlsplit(self._config.redirect_uri)
callback_target = (callback.scheme, callback.hostname, callback.port, callback.path)
expected_target = (expected.scheme, expected.hostname, expected.port, expected.path)
if callback_target != expected_target:
raise AuthenticationError("OAuth callback used an unexpected redirect URI")
query = parse_qs(callback.query)
if query.get("state") != [pending.state]:
raise AuthenticationError("OAuth callback state did not match")
if "error" in query:
raise AuthenticationError("Authorization server rejected login")
code = query.get("code", [None])[0]
if code is None:
raise AuthenticationError("OAuth callback did not contain a code")
result = await self._client.fetch_token(
self._config.token_endpoint,
code=code,
code_verifier=pending.code_verifier,
)
token = cast("OAuthToken", dict(result))
await self._store.save(token)
return token
async def aclose(self) -> None:
await self._client.aclose()
```
Do not persist `PendingAuthorization`: `state` and `code_verifier` are short-lived, single-attempt values. Define `AuthenticationError` in the application's stable error taxonomy and keep callback query values out of its message.
If a browser or loopback listener is impractical and the provider exposes it, use the standardized [Device Authorization Grant](https://www.rfc-editor.org/rfc/rfc8628). Respect the server-provided polling interval, `slow_down`, expiration, and cancellation behavior. Do not invent a device flow against a provider that does not advertise or document one.
## Authorization Server Metadata
Prefer [OAuth 2.0 Authorization Server Metadata](https://www.rfc-editor.org/rfc/rfc8414) or OpenID Connect discovery when the provider supports it. Validate that discovered metadata belongs to the configured issuer and require HTTPS for non-loopback endpoints.
Use explicit endpoints when discovery is unavailable or when a controlled deployment intentionally pins them. Do not mix endpoints discovered from one issuer with configuration from another.
## Token Lifecycle
Centralize expiry and refresh decisions in `TokenManager`:
```text
load token
├── missing -> authentication required
├── valid beyond refresh leeway -> return access token
└── expired or near expiry
└── acquire refresh lock
├── reload token
├── return it if another task refreshed it
└── refresh, atomically persist the full response, and return it
```
Use a small expiry leeway, commonly 60 seconds, to avoid starting a request with a token that expires in transit. For a single-process async CLI, an `asyncio.Lock` is sufficient for in-process refresh coordination. If multiple processes can share one token store, add storage-level coordination or optimistic versioning; an in-process lock cannot prevent cross-process races.
Preserve a refresh token when the server omits it from a refresh response, but replace it whenever rotation returns a new one. Save the newly returned token as one atomic state update so an older writer cannot restore a superseded refresh token.
Classify missing, expired-without-refresh, rejected-refresh, and revoked credentials as authentication failures with a clear path to log in again. Do not turn refresh failures into anonymous API calls.
A small refresher adapter and token manager keep Authlib details out of storage and API resources:
```python
import asyncio
import time
from collections.abc import Callable
class AuthenticationError(RuntimeError):
pass
class AuthlibTokenRefresher:
def __init__(self, config: OAuthConfig) -> None:
self._config = config
async def refresh(self, token: OAuthToken) -> OAuthToken:
refresh_token = token.get("refresh_token")
if refresh_token is None:
raise AuthenticationError("Login is required")
async with AsyncOAuth2Client(
client_id=self._config.client_id,
token=dict(token),
token_endpoint_auth_method="none",
) as client:
result = await client.refresh_token(
self._config.token_endpoint,
refresh_token=refresh_token,
)
refreshed = cast("OAuthToken", dict(result))
refreshed.setdefault("refresh_token", refresh_token)
return refreshed
class TokenManager:
def __init__(
self,
store: TokenStore,
refresher: AuthlibTokenRefresher,
*,
refresh_leeway: float = 60.0,
clock: Callable[[], float] = time.time,
) -> None:
self._store = store
self._refresher = refresher
self._refresh_leeway = refresh_leeway
self._clock = clock
self._lock = asyncio.Lock()
def _is_usable(self, token: OAuthToken) -> bool:
expires_at = token.get("expires_at")
return expires_at is None or expires_at > self._clock() + self._refresh_leeway
async def get_valid_access_token(self, *, force_refresh: bool = False) -> str:
token = await self._store.load()
if token is None:
raise AuthenticationError("Login is required")
if not force_refresh and self._is_usable(token):
return token["access_token"]
async with self._lock:
token = await self._store.load()
if token is None:
raise AuthenticationError("Login is required")
if not force_refresh and self._is_usable(token):
return token["access_token"]
refreshed = await self._refresher.refresh(token)
await self._store.save(refreshed)
return refreshed["access_token"]
```
This lock covers one process. Replace or augment it with storage-level compare-and-swap or an inter-process lock when several processes share the same credential entry.
## Authenticated HTTP Transport
The transport should:
1. Obtain a valid access token before sending a protected request.
2. Inject the authorization header without exposing the token to API resource code.
3. Apply the project's normal timeout, TLS, proxy, retry, and error-mapping policy.
4. Optionally react to one `401` by forcing one synchronized refresh and replaying the request once.
5. Raise an authentication error if the replay is still unauthorized.
Do not refresh blindly on every `401`; unauthorized responses can indicate revocation, malformed credentials, the wrong audience, or another authentication failure. Never create an unbounded refresh or request loop. Replay only requests whose body can be safely regenerated, and do not treat `403` as an expiry signal.
[HTTPX2 custom authentication](https://pydantic.dev/docs/httpx2/advanced/authentication/#custom-authentication-schemes) is a compact way to apply the token manager to every API request. This example retries one bodyless, read-only request after a synchronized refresh and leaves all other `401` responses untouched:
```python
import httpx2
class OAuthAuth(httpx2.Auth):
requires_response_body = True
def __init__(self, tokens: TokenManager) -> None:
self._tokens = tokens
def sync_auth_flow(self, request: httpx2.Request):
raise RuntimeError("OAuthAuth requires httpx2.AsyncClient")
yield request
async def async_auth_flow(self, request: httpx2.Request):
access_token = await self._tokens.get_valid_access_token()
request.headers["Authorization"] = f"Bearer {access_token}"
response = yield request
if response.status_code != 401 or request.method not in {"GET", "HEAD", "OPTIONS"}:
return
access_token = await self._tokens.get_valid_access_token(force_refresh=True)
request.headers["Authorization"] = f"Bearer {access_token}"
yield request
class ApiClient:
def __init__(self, base_url: str, tokens: TokenManager) -> None:
self._http = httpx2.AsyncClient(
base_url=base_url,
auth=OAuthAuth(tokens),
timeout=httpx2.Timeout(20.0, connect=5.0),
)
async def get_project(self, project_id: str) -> dict[str, object]:
response = await self._http.get(f"/projects/{project_id}")
response.raise_for_status()
value = response.json()
if not isinstance(value, dict):
raise ValueError("Expected an object response")
return value
async def aclose(self) -> None:
await self._http.aclose()
```
For streaming requests, uploads, or state-changing methods, omit automatic replay unless the API supplies an idempotency mechanism and the request body can be rebuilt. Map the final HTTPX2 response or exception into application errors before it reaches a CLI command.
## Scopes And Token Restrictions
- Request only scopes needed by the CLI's supported operations.
- Keep scopes explicit in configuration and stable in tests.
- Request offline access or equivalent provider-specific scope only when refresh tokens are needed.
- Use audience or resource restrictions when supported.
- Detect when stored authorization lacks scopes required by a command and direct the user through deliberate reauthorization rather than quietly broadening every login.
## CLI Surface
Expose a small authentication surface consistent with the existing command framework:
```text
mycli login
mycli logout
mycli auth status
```
`login` starts interactive authorization and reports only progress and outcome. `logout` deletes local token state and uses the provider's revocation endpoint when supported; explain if remote revocation fails after local deletion. `auth status` may show account identity, granted scopes, issuer, and expiry, but never a token or authorization code.
A manual `auth refresh` command is optional and primarily diagnostic. Normal API commands should not require users to manage refresh timing.
## Suggested Package Shape
Adapt names to the existing project rather than forcing this exact tree:
```text
src/mycli/
├── cli.py
├── errors.py
├── api/
│ ├── client.py
│ ├── transport.py
│ └── resources/
└── auth/
├── config.py
├── manager.py
├── token.py
└── store.py
```
Keep token models independent from provider client objects so storage, tests, and API code do not inherit Authlib's internal representation as a public application contract.
## Branching Guidance
- If the provider supports loopback redirects: use Authorization Code with PKCE and an ephemeral `127.0.0.1` listener.
- If the execution environment cannot open a browser or accept a loopback callback: use Device Authorization Grant only when the provider supports it.
- If the API is called by unattended automation rather than a person: use the provider's machine-to-machine grant and credential mechanism; do not reuse an interactive user's refresh token as service identity.
- If the provider supports discovery: derive endpoints from validated issuer metadata; otherwise pin explicit HTTPS endpoints.
- If requests are concurrent in one process: serialize refresh with a lock and re-read state after acquisition.
- If token state is shared across processes: use inter-process coordination and atomic persistence.
- If an existing CLI already has HTTP and configuration abstractions: integrate at those boundaries instead of replacing the command framework or resource layer.
## Tests And Verification
Test protocol behavior without depending on a live identity provider:
- Login creates fresh state and verifier values and uses `S256`.
- Callback handling accepts the expected state and rejects missing, mismatched, duplicate, timed-out, and OAuth-error callbacks.
- The listener binds only to loopback and always shuts down.
- Token exchange uses the same redirect URI and verifier as authorization.
- Valid tokens bypass refresh; near-expiry tokens refresh once.
- Concurrent requests cause one refresh and all callers observe the persisted replacement token.
- Refresh-token rotation cannot be overwritten by stale state.
- A `401` causes at most one eligible replay; a second `401` fails.
- Status and error output remain useful without revealing token values.
- Logout clears local state and handles optional remote revocation explicitly.
Use deterministic clocks, fake token stores, and [`httpx2.MockTransport`](https://pydantic.dev/docs/httpx2/advanced/transports/#mock-transports) for unit tests. Add a provider integration test only when the project has suitable isolated credentials and CI secret handling.
## Completion Checks
1. The CLI is registered and implemented as a public client without an embedded secret.
2. Interactive login uses Authorization Code with PKCE `S256`, fresh state, exact redirect validation, and a bounded loopback listener.
3. Device authorization is conditional on provider support and follows server polling instructions.
4. Token storage is replaceable, protected, atomic, and absent from logs and normal output.
5. One component owns expiry, synchronized refresh, and refresh-token rotation.
6. API resources remain independent of OAuth protocol and storage details.
7. Scopes and audience are minimal and explicit.
8. Authentication retries are bounded and replay only eligible requests.
9. Login, logout, status, refresh, concurrency, callback rejection, and redaction paths have focused verification.
10. Provider-specific behavior and installed dependency versions are checked against current primary documentation.
@@ -66,7 +66,6 @@ Choose one of these patterns:
- Read selected supporting files at `skill://<skill-name>/<supporting-path>`. - Read selected supporting files at `skill://<skill-name>/<supporting-path>`.
2. Discovery-first strategy: 2. Discovery-first strategy:
- List resources, compare native main-resource names and descriptions, then load the best matching `SKILL.md`. - List resources, compare native main-resource names and descriptions, then load the best matching `SKILL.md`.
- In tool-only clients, use only `list_resources` and `read_resource` for the same sequence.
### Authoring guidance for shims ### Authoring guidance for shims
@@ -7,6 +7,8 @@ Use this page for MCP client setup, operational tools, and integration reference
!!! info "VS Code MCP docs" !!! info "VS Code MCP docs"
- [VS Code MCP servers overview](https://code.visualstudio.com/docs/agent-customization/mcp-servers) - [VS Code MCP servers overview](https://code.visualstudio.com/docs/agent-customization/mcp-servers)
- [VS Code MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration) - [VS Code MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)
- [VS Code MCP developer guide](https://code.visualstudio.com/docs/agents/guides/mcp-developer-guide)
- [VS Code MCP Apps support](https://code.visualstudio.com/blogs/2026/01/26/mcp-apps-support)
- [VS Code Copilot customization overview](https://code.visualstudio.com/docs/copilot/customization/overview) - [VS Code Copilot customization overview](https://code.visualstudio.com/docs/copilot/customization/overview)
## Debugging and Inspection ## Debugging and Inspection
@@ -13,8 +13,19 @@ Use this page for implementation-oriented links across MCP SDKs and FastMCP.
!!! info "FastMCP sources" !!! info "FastMCP sources"
- [FastMCP project documentation](https://gofastmcp.com/) - [FastMCP project documentation](https://gofastmcp.com/)
- [FastMCP GitHub repository](https://github.com/jlowin/fastmcp) - [FastMCP server identity and behavior](https://gofastmcp.com/servers/server)
- [FastMCP examples directory](https://github.com/jlowin/fastmcp/tree/main/examples) - [FastMCP providers overview](https://gofastmcp.com/servers/providers/overview)
- [FastMCP custom providers](https://gofastmcp.com/servers/providers/custom)
- [FastMCP skills provider](https://gofastmcp.com/servers/providers/skills)
- [FastMCP tools and annotations](https://gofastmcp.com/servers/tools)
- [FastMCP resources and templates](https://gofastmcp.com/servers/resources)
- [FastMCP prompts](https://gofastmcp.com/servers/prompts)
- [FastMCP filesystem provider](https://gofastmcp.com/servers/providers/filesystem)
- [FastMCP argument completion](https://gofastmcp.com/servers/completions)
- [FastMCP component icons](https://gofastmcp.com/servers/icons)
- [FastMCP Apps](https://gofastmcp.com/apps/overview)
- [FastMCP GitHub repository](https://github.com/PrefectHQ/fastmcp)
- [FastMCP examples directory](https://github.com/PrefectHQ/fastmcp/tree/main/examples)
- [FastMCP PyPI package](https://pypi.org/project/fastmcp/) - [FastMCP PyPI package](https://pypi.org/project/fastmcp/)
## Server Implementation Patterns ## Server Implementation Patterns
@@ -0,0 +1,73 @@
---
name: nicegui
description: 'Build, review, debug, configure, deploy, and package NiceGUI applications. Use for FastAPI or Uvicorn integration, ui.run settings, native mode, Docker or executable deployment, app factories and lifespan, thin pages and reusable component factories, bindable dataclass handles, ui.refreshable methods, ui.* components, Quasar props/events/slots, Tailwind layout, colors, bindings, editable ui.table cells, uploads/forms/live updates, or version-specific source research.'
---
# NiceGUI Application Guide
Use this skill to choose the smallest supporting reference for a NiceGUI task. The pages cover different ownership boundaries; do not load the whole reference set.
## Workflow
1. Inspect the target project's pinned NiceGUI version, entry point, and existing page/component patterns.
2. Match the request to one row in the routing table and load that primary reference.
3. Load the optional companion only when the task crosses the boundary named in the last column.
4. Prefer NiceGUI's typed constructor, binding, or helper API; descend to Quasar props, events, slots, or methods only when the wrapper does not expose the required behavior.
5. Validate the changed behavior with a focused test. For visual work, also check the supported mobile, landscape desktop, and portrait desktop viewports.
## Task Routing
| Task or symptom | Load first | Add only when |
| --- | --- | --- |
| Choose package boundaries, dependency direction, thin page composition, reusable component factories, returned dataclass component handles, page registration, health routes, or optional subsystem placement | [application architecture](./references/architecture.md) | Add [binding dataclasses](./references/binding-dataclasses.md) for the component handle's binding graph or [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) for concrete ASGI ownership. |
| Decide between `ui.run()` and `ui.run_with()`, compose a parent FastAPI app, define lifespan ordering, build an app factory, configure typed settings, expose a project script, or handle reload/workers | [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) | Add [configuration and deployment](./references/configuration-and-deployment.md) for concrete `ui.run` options, hosting, native mode, or packaging. |
| Configure `ui.run`, consume `app.urls`, select NiceGUI environment variables, run behind Docker or a reverse proxy, enable HTTPS, build a native app, package with PyInstaller or Nuitka, or evaluate NiceGUI On Air | [configuration and deployment](./references/configuration-and-deployment.md) | Add [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) when a parent ASGI app, app factory, lifespan, reload, or workers own part of startup. |
| Choose a `ui.*` constructor, binding, Quasar prop, event, slot, or frontend method; diagnose model events, event payloads, scoped-slot props, detached popups, `ui.select`, or `ui.icon` | [component mechanics](./references/component-mechanics.md) | Add [source documentation](./references/source-documentation.md) when the installed wrapper or bundled Quasar version must be verified. |
| Build page shells, rows, columns, grids, widths, overflow, responsive reflow, typography, font loading, static assets, or deliberate scaling | [page structure, typography, and scaling](./references/styling-and-customization.md) | Add [component mechanics](./references/component-mechanics.md) when layout depends on a Quasar prop, slot, popup, or generated component structure. |
| Configure `app.colors()`, `ui.colors()`, semantic or fixed Quasar colors, custom color names, component color values, CSS color variables, or `ui.dark_mode()` | [NiceGUI and Quasar color theming](./references/colors-and-quasar-theming.md) | Add [page structure, typography, and scaling](./references/styling-and-customization.md) only when the task also changes physical layout or CSS loading. |
| Model typed page or component state with `binding.bindable_dataclass`, return a bound component handle, understand propagation and transform direction, bind nested values, avoid active-link polling, or design projection/persistence rollback | [binding dataclasses](./references/binding-dataclasses.md) | Add [application architecture](./references/architecture.md) for the render-factory and thin-page boundary or [component mechanics](./references/component-mechanics.md) for browser-originated proposals. |
| Customize `ui.table` or QTable columns, formatting, classes, props, responsive density, toolbar controls, visible columns, empty states, named slots, or frontend methods | [table customization](./references/table-customization.md) | Add [editable tables](./references/tables.md) only when cells also accept server-authoritative edits. |
| Make `ui.table` cells editable with stable row keys, dataframe projections, row-scoped dataclasses, validation, touched rows, selection-preserving refresh, or `QPopupEdit` | [editable tables](./references/tables.md) | Follow its links to binding or component mechanics only when changing the underlying projection or event bridge. |
| Implement uploads, form submission, SSE versus WebSockets, background jobs, duplicate-submit guards, or `@ui.refreshable` and `@ui.refreshable_method` component regions | [interaction patterns](./references/interaction-patterns.md) | Add [application architecture](./references/architecture.md) for the reusable component contract or [binding dataclasses](./references/binding-dataclasses.md) when state propagation itself is the problem. |
| Build or explain URL-backed tabs, persistent tab panels, `ui.sub_pages` route adapters, browser-history synchronization, or parameterized routes that share one tab | [URL-backed tabs with sub pages](./references/tabbed-subpages.md) | Add [binding dataclasses](./references/binding-dataclasses.md) only when the route-backed state grows beyond the single field shown in the example. |
| Investigate upload errors, async UI races, stale assets, navigation/state drift, or perform a compact production-readiness review | [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md) | Follow the symptom to one detailed reference above. |
| Verify a framework claim against primary NiceGUI, FastAPI, Uvicorn, Tailwind, Quasar, SQLAlchemy, Pydantic, or LangGraph documentation | [source documentation](./references/source-documentation.md) | Use a task page first when implementation guidance, not source lookup, is needed. |
## Boundary Rules
- Use [application architecture](./references/architecture.md) for module ownership, not for page geometry or low-level component behavior.
- Use [FastAPI and Uvicorn startup](./references/fastapi-uvicorn-startup.md) to decide which process owns startup; use [configuration and deployment](./references/configuration-and-deployment.md) after that decision for runtime, native, hosting, and packaging settings.
- Keep page functions thin: compose page shells and returned component handles there; keep each component's element tree, bindings, callbacks, and bounded refreshes in its render factory or component object.
- Use [page structure, typography, and scaling](./references/styling-and-customization.md) for physical layout. Use [component mechanics](./references/component-mechanics.md) for the behavior crossing NiceGUI, Quasar, Vue, and browser boundaries.
- Start read-only table presentation and QTable control work in [table customization](./references/table-customization.md); keep editable state and validation in [editable tables](./references/tables.md).
- Use [binding dataclasses](./references/binding-dataclasses.md) for the binding graph and Python model projections. Use [interaction patterns](./references/interaction-patterns.md) for user workflows such as upload, submit, refresh, streaming, and background work.
- Start editable-table work in [editable tables](./references/tables.md). It already identifies the exact binding and event sections needed by that pattern.
- Treat [source documentation](./references/source-documentation.md) as a source index, not as an implementation workflow.
## Runnable Examples
Load an example only when its exact mechanic matches the task:
- [binding transforms](./examples/data_binding.py): `bindable_dataclass`, `ui.date`, and typed `forward`/`backward` conversion.
- [select events](./examples/select_events.py): `on_change`, generic Quasar events, `update:model-value`, browser-to-Python payload forwarding, and programmatic value changes.
- [table customization](./examples/table_customization.py): raw-value sorting with cosmetic prefix, suffix, and datetime formatting; dynamic classes; QTable props; toolbar and cell slots; filtering; visible columns; and empty states.
- [editable table](./examples/editable_table.py): dataframe-to-row state, named QTable cell slots, controlled editors, dialog-based whole-row save/cancel edits, Python validation, touched rows, and canonical row refresh.
- [tabbed sub-pages](./examples/tab_spa.py): a persistent shell with URL-backed tabs, tab panels, browser-history navigation, and retained state for a parameterized report route. See the [reference explanation](./references/tabbed-subpages.md) for the ownership model and behavioral boundaries.
## Defaults That Span References
- Keep composition, transport, services, pages, and components directionally separated.
- Prefer reusable render functions that return typed component handles; use bindable dataclass fields for the state intentionally exposed to composition code.
- Keep business logic out of UI components and event handlers.
- Avoid blocking I/O and CPU-heavy work in the UI event loop.
- Prefer event-driven updates and explicit refreshes to unrelated polling.
- Keep browser-originated values as proposals; validate and normalize them in Python before mutating authoritative state.
- Prefer NiceGUI context managers and `ui.*` elements over raw Vue templates. Keep application logic and authoritative state in Python; use minimal browser expressions only for scoped-slot values or client-only behavior, following [Python-owned slot composition](./references/component-mechanics.md#prefer-python-owned-composition).
- For each presentation requirement, check the component's typed Python arguments and helpers before using `.props(...)` or `.classes(...)`. Create an application class and add CSS only when no Python API, documented component prop or slot, or existing utility class can express the requirement.
- Use NiceGUI context managers for element structure and Tailwind for generic layout, spacing, responsive behavior, and typography. Keep Quasar classes for semantic palette roles or component-specific geometry, and use Quasar props for component behavior and density; see [combining Tailwind with Quasar utilities](./references/styling-and-customization.md#combine-tailwind-with-quasar-utilities-deliberately).
- Provide loading, success, and failure states for user-triggered work.
- Verify component-specific behavior against the installed NiceGUI version and its bundled Quasar version.
## Completion Check
Before finishing, distinguish target-repository facts from reference recommendations, cite the supporting page used for framework-specific claims, state unresolved assumptions, and report the focused behavior and viewport checks performed.
@@ -0,0 +1,30 @@
from dataclasses import field
from datetime import date
from nicegui import binding
from nicegui import ui
@binding.bindable_dataclass
class ReportFilters:
start_on: date = field(default_factory=date.today)
page_size: int = 25
filters = ReportFilters()
ui.date().bind_value(
filters,
"start_on",
forward=date.fromisoformat, # control str -> model date
backward=date.isoformat, # model date -> control str
)
ui.label().bind_text_from(
filters,
"start_on",
backward=lambda value: f"Starting {value:%d %B %Y}",
)
if __name__ in {"__main__", "__mp_main__"}:
ui.run(port=8888, reload=True)
@@ -0,0 +1,338 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "nicegui==3.16.0",
# "pandas",
# "pydantic>=2",
# ]
# ///
from collections.abc import Callable
from dataclasses import dataclass
from dataclasses import field
import pandas as pd
from nicegui import binding
from nicegui import events
from nicegui import ui
from pydantic import BaseModel
from pydantic import ValidationError
from pydantic import field_validator
STATUS_OPTIONS = ["draft", "active", "archived"]
EDITABLE_FIELDS = ("name", "quantity", "status")
type TableValue = str | int
type TableRow = dict[str, TableValue]
class RowEditDraft(BaseModel):
name: str
quantity: int
status: str
@field_validator("name")
@classmethod
def validate_name(cls, value: str) -> str:
if not (name := value.strip()):
raise ValueError("Name is required")
return name
@field_validator("quantity", mode="before")
@classmethod
def validate_quantity(cls, value: object) -> int:
if isinstance(value, bool) or value is None:
raise TypeError("Quantity must be an integer")
if not isinstance(value, (int, float, str)):
raise TypeError("Quantity must be an integer")
if isinstance(value, float) and not value.is_integer():
raise ValueError("Quantity must be an integer")
try:
quantity = int(value)
except (TypeError, ValueError, OverflowError) as error:
raise ValueError("Quantity must be an integer") from error
if not 0 <= quantity <= 1_000:
raise ValueError("Quantity must be between 0 and 1000")
return quantity
@field_validator("status")
@classmethod
def validate_status(cls, value: str) -> str:
if value not in STATUS_OPTIONS:
raise ValueError("Unknown status")
return value
@dataclass(slots=True)
class RowEditorDialog:
open_for_row_id: Callable[[int], None]
def _validation_message(error: ValidationError) -> str:
return str(error.errors()[0]["msg"])
@binding.bindable_dataclass
class EditableRow:
id: int
name: str
quantity: int
status: str
table_row: TableRow = field(init=False, repr=False)
touched: bool = False
def __post_init__(self) -> None:
self.table_row = {
"id": self.id,
"name": self.name,
"quantity": self.quantity,
"status": self.status,
}
for field_name in EDITABLE_FIELDS:
binding.bind_to(
self,
field_name,
self.table_row,
field_name,
other_strict=True,
)
def to_draft(self) -> RowEditDraft:
return RowEditDraft(name=self.name, quantity=self.quantity, status=self.status)
def validate_update(self, updates: dict[str, object]) -> RowEditDraft:
base_values = self.to_draft().model_dump()
return RowEditDraft.model_validate({**base_values, **updates})
def apply_draft(self, draft: RowEditDraft) -> None:
self.name = draft.name
self.quantity = draft.quantity
self.status = draft.status
@dataclass(slots=True)
class EditableTableState:
rows_by_id: dict[int, EditableRow]
def row(self, row_id: int) -> EditableRow | None:
return self.rows_by_id.get(row_id)
def table_rows(self) -> list[TableRow]:
return [row.table_row for row in self.rows_by_id.values()]
def touched_rows(self) -> list[EditableRow]:
return [row for row in self.rows_by_id.values() if row.touched]
def dataframe_to_state(dataframe: pd.DataFrame) -> EditableTableState:
required_columns = {"id", *EDITABLE_FIELDS}
missing_columns = required_columns.difference(dataframe.columns)
if missing_columns:
raise ValueError(f"Missing columns: {sorted(missing_columns)}")
if not dataframe["id"].is_unique:
raise ValueError("The id column must contain unique row keys")
rows_by_id: dict[int, EditableRow] = {}
for record in dataframe.to_dict(orient="records"):
row = EditableRow(
id=int(record["id"]),
name=str(record["name"]),
quantity=int(record["quantity"]),
status=str(record["status"]),
)
if row.status not in STATUS_OPTIONS:
raise ValueError(f"Unknown status {row.status!r}")
if row.id in rows_by_id:
raise ValueError("Row keys must remain unique after normalization")
rows_by_id[row.id] = row
return EditableTableState(rows_by_id)
def render_row_editor_dialog(
state: EditableTableState,
refresh_table: Callable[[], None],
) -> RowEditorDialog:
selected_row_id: int | None = None
with ui.dialog() as edit_dialog, ui.card().classes("w-96"):
dialog_heading = ui.label("Edit row")
draft_name = ui.input("Name")
draft_quantity = ui.number("Quantity", min=0, max=1_000, precision=0)
draft_status = ui.select(STATUS_OPTIONS, label="Status")
with ui.row().classes("w-full justify-end"):
ui.button("Cancel", on_click=edit_dialog.close).props("flat")
def save_dialog_edit() -> None:
nonlocal selected_row_id
try:
if selected_row_id is None:
raise ValueError("Select a row before saving")
row_state = state.row(selected_row_id)
if row_state is None:
raise ValueError("This row no longer exists")
draft = row_state.validate_update(
{
"name": draft_name.value,
"quantity": draft_quantity.value,
"status": draft_status.value,
},
)
row_state.apply_draft(draft)
row_state.touched = True
edit_dialog.close()
except ValidationError as error:
ui.notify(_validation_message(error), type="negative")
except ValueError as error:
ui.notify(str(error), type="negative")
finally:
refresh_table()
ui.button("Save", icon="save", on_click=save_dialog_edit)
def open_for_row_id(row_id: int) -> None:
nonlocal selected_row_id
row_state = state.row(row_id)
if row_state is None:
ui.notify("This row no longer exists", type="negative")
return
selected_row_id = row_id
draft = row_state.to_draft()
dialog_heading.set_text(f"Edit row {row_id}")
draft_name.set_value(draft.name)
draft_quantity.set_value(draft.quantity)
draft_status.set_value(draft.status)
edit_dialog.open()
return RowEditorDialog(open_for_row_id=open_for_row_id)
def render_table(dataframe: pd.DataFrame) -> EditableTableState:
state = dataframe_to_state(dataframe)
columns = [
{"name": "name", "label": "Name", "field": "name", "align": "left"},
{"name": "quantity", "label": "Quantity", "field": "quantity", "align": "right"},
{"name": "status", "label": "Status", "field": "status", "align": "left"},
{"name": "actions", "label": "Actions", "field": "id", "align": "center"},
]
table = ui.table(
columns=columns,
rows=state.table_rows(),
row_key="id",
selection="multiple",
pagination=10,
).classes("w-120")
def refresh_table() -> None:
table.update_rows(state.table_rows(), clear_selection=False)
def apply_inline_edit(event: events.GenericEventArguments) -> None:
try:
raw_row_id, raw_field, raw_value = event.args
row_id = int(raw_row_id)
field_name = str(raw_field)
if field_name not in EDITABLE_FIELDS:
raise ValueError(f"Field {field_name!r} is not editable")
row_state = state.row(row_id)
if row_state is None:
raise ValueError("This row no longer exists")
draft = row_state.validate_update({field_name: raw_value})
row_state.apply_draft(draft)
row_state.touched = True
except ValidationError as error:
ui.notify(_validation_message(error), type="negative")
except (TypeError, ValueError) as error:
ui.notify(str(error), type="negative")
finally:
refresh_table()
def show_changes() -> None:
changed_rows = state.touched_rows()
if not changed_rows:
ui.notify("No rows changed")
return
for row in changed_rows:
ui.notify(f"Changed row: {row.id}: {row.name}, quantity {row.quantity}, status {row.status}")
row_editor = render_row_editor_dialog(state, refresh_table)
_add_slots(
table,
apply_inline_edit,
row_editor.open_for_row_id,
)
with ui.row().classes("w-120 justify-end"):
ui.button("Show changes", icon="edit_note", on_click=show_changes)
return state
def open_dialog_for_row(open_editor: Callable[[int], None], event: events.GenericEventArguments) -> None:
try:
row_id = int(event.args)
open_editor(row_id)
except (TypeError, ValueError):
ui.notify("Invalid row key", type="negative")
def _add_slots(
table: ui.table,
apply_inline_edit: Callable[[events.GenericEventArguments], None],
open_editor: Callable[[int], None],
):
with table.add_slot("body-cell-name"), table.cell("name"):
name_input = ui.input().props(remove="value")
name_input.props(':value="props.value" dense borderless debounce=400').on(
"update:value",
handler=apply_inline_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)",
)
with table.add_slot("body-cell-quantity"), table.cell("quantity"):
ui.number(min=0, max=1_000).props(':model-value="props.value" dense borderless debounce=400').on(
"update:model-value",
handler=apply_inline_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)",
)
with table.add_slot("body-cell-status"), table.cell("status"):
ui.select(STATUS_OPTIONS).props(':model-value="props.value" dense borderless options-dense').on(
"update:model-value",
handler=apply_inline_edit,
js_handler="(option) => emit(props.row.id, props.col.name, option.label)",
)
with table.add_slot("body-cell-actions"), table.cell("actions"):
edit_button = ui.button(icon="edit")
edit_button.props('flat round dense color=primary aria-label="Edit row"')
edit_button.tooltip("Edit this row").on(
"click",
handler=lambda event: open_dialog_for_row(open_editor, event),
js_handler="() => emit(props.row.id)",
)
if __name__ in {"__main__", "__mp_main__"}:
items = pd.DataFrame(
[
{"id": 101, "name": "Desk", "quantity": 4, "status": "active"},
{"id": 102, "name": "Lamp", "quantity": 12, "status": "draft"},
{"id": 103, "name": "Chair", "quantity": 8, "status": "active"},
{"id": 104, "name": "Shelf", "quantity": 3, "status": "draft"},
{"id": 105, "name": "Monitor", "quantity": 15, "status": "active"},
{"id": 106, "name": "Keyboard", "quantity": 20, "status": "active"},
{"id": 107, "name": "Mouse", "quantity": 24, "status": "active"},
{"id": 108, "name": "Dock", "quantity": 6, "status": "archived"},
{"id": 109, "name": "Cable", "quantity": 40, "status": "draft"},
{"id": 110, "name": "Stand", "quantity": 10, "status": "active"},
]
)
table_state = render_table(items)
ui.run(port=8888, reload=True)
@@ -0,0 +1,64 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "nicegui==3.16.0",
# ]
# ///
from datetime import UTC
from datetime import datetime
from nicegui import events
from nicegui import ui
OPTIONS = {
"python": "Python",
"typescript": "TypeScript",
"rust": "Rust",
}
with ui.card().classes("w-140 max-w-full"):
ui.label("Select event mechanics").classes("text-xl font-semibold")
event_log = ui.log(max_lines=12).classes("w-full h-64")
def record(event_name: str, payload: object) -> None:
timestamp = datetime.now(UTC).astimezone().strftime("%H:%M:%S")
event_log.push(f"{timestamp} {event_name}: {payload!r}")
def handle_change(event: events.ValueChangeEventArguments) -> None:
record("on_change event.value", event.value)
def handle_model_update(event: events.GenericEventArguments) -> None:
record("js_handler -> handler event.args", event.args)
language = (
ui.select(
options=OPTIONS,
value="python",
label="Language",
on_change=handle_change,
with_input=True,
clearable=True,
)
.props("outlined options-dense")
.classes("w-full text-h6")
)
language.on("popup-show", lambda: record("popup-show", None), args=[])
language.on("popup-hide", lambda: record("popup-hide", None), args=[])
# This only fires when the value is changed from the browser side (not from the button)
language.on(
"update:model-value",
handler=handle_model_update,
js_handler="(...args) => emit(...args)",
)
with ui.row().classes("w-full justify-end"):
ui.button("Set Rust", on_click=lambda: language.set_value("rust"))
ui.button("Clear log", on_click=event_log.clear).props("flat")
if __name__ in {"__main__", "__mp_main__"}:
ui.run(port=8888, reload=True)
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "nicegui==3.16.0",
# ]
# ///
"""Demonstrate URL-backed tabs with persistent parameterized-route state."""
from __future__ import annotations
from collections.abc import Callable
from urllib.parse import urlsplit
from nicegui import binding
from nicegui import events
from nicegui import ui
from nicegui.elements.tabs import Tab
from nicegui.elements.tabs import TabPanel
type PageBuilder = Callable[..., None]
DEFAULT_REPORT_PATH = "/reports/a"
REPORTS_TAB = "reports"
TAB_ROUTES = frozenset({"/", "/projects", "/settings"})
def page_heading(title: str, description: str) -> None:
"""Render a shared heading for sub-page content."""
with ui.column().classes("w-full gap-1"):
ui.label(title).classes("text-3xl font-semibold text-stone-900")
ui.label(description).classes("text-base text-stone-600")
def overview_page() -> None:
"""Render the overview sub-page."""
with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4 py-8"):
page_heading("Overview", "A quick read on the workspace today.")
metrics = (
("Active projects", "8", "folder_open", "primary"),
("Tasks completed", "24", "task_alt", "positive"),
("Needs attention", "3", "error_outline", "warning"),
)
with ui.grid().classes("w-full grid-cols-1 gap-4 md:grid-cols-3"):
for label, value, icon, color in metrics:
with ui.card().classes("w-full p-5 gap-3"):
with ui.row().classes("w-full items-center justify-between"):
ui.label(label).classes("text-sm font-medium text-stone-600")
ui.icon(icon, color=color).classes("text-2xl")
ui.label(value).classes("text-3xl font-semibold text-stone-900")
def projects_page() -> None:
"""Render the projects sub-page."""
with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4 py-8"):
page_heading("Projects", "Each route builds its own content inside the shared shell.")
with ui.list().props("bordered separator").classes("w-full bg-white rounded"):
for name, status, color in (
("Client portal", "On track", "positive"),
("Mobile refresh", "In review", "primary"),
("Data migration", "Blocked", "negative"),
):
with ui.item():
with ui.item_section().props("avatar"):
ui.icon("folder", color=color)
with ui.item_section():
ui.item_label(name)
ui.item_label(status).props("caption")
with ui.item_section().props("side"):
ui.badge(status, color=color)
def report_page(state: NavigationState) -> None:
"""Render report content bound to the active route parameter."""
with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4 py-8"):
with ui.column().classes("w-full gap-1"):
ui.label().bind_text_from(
state,
"active_report_path",
backward=lambda path: f"Report {report_id_from_path(path).upper()}",
).classes("text-3xl font-semibold text-stone-900")
ui.label("The report ID is injected from the URL path.").classes("text-base text-stone-600")
with ui.card().classes("w-full max-w-2xl p-5 gap-4"):
ui.label("Parameterized route").classes("text-xl font-semibold text-stone-900")
ui.label().bind_text_from(
state,
"active_report_path",
backward=lambda path: f"Loaded {path}",
).classes("text-stone-600")
with ui.row().classes("gap-2"):
ui.button("Report A", on_click=lambda: ui.navigate.to("/reports/a")).props("outline")
ui.button("Report B", on_click=lambda: ui.navigate.to("/reports/b")).props("outline")
def settings_page() -> None:
"""Render the settings sub-page."""
with ui.column().classes("w-full max-w-6xl mx-auto gap-6 px-4 py-8"):
page_heading("Settings", "Controls here are recreated when this sub-page is opened.")
with ui.card().classes("w-full max-w-2xl p-5 gap-4"):
ui.label("Notifications").classes("text-xl font-semibold text-stone-900")
ui.switch("Weekly summary", value=True)
ui.switch("Project status changes", value=True)
ui.switch("Product announcements", value=False)
def normalize_route(path: str) -> str:
"""Extract and normalize the path portion of a route."""
return urlsplit(path).path.rstrip("/") or "/"
def tab_name_for_route(route: str) -> str:
"""Return the tab name associated with a concrete route."""
if route.startswith("/reports/"):
return REPORTS_TAB
return route if route in TAB_ROUTES else "/"
@binding.bindable_dataclass
class NavigationState:
"""Store client-local navigation state for parameterized tabs."""
active_report_path: str = DEFAULT_REPORT_PATH
type TabHandler = events.ValueChangeEventArguments[str | Tab | TabPanel | None]
def report_id_from_path(path: str) -> str:
"""Extract the report ID from a normalized report route."""
return path.rsplit("/", maxsplit=1)[-1]
def create_tabs(state: NavigationState, initial_route: str) -> ui.tabs:
"""Create route-aware tabs and retain the last selected report."""
def navigate(event: TabHandler) -> None:
"""Navigate to the route represented by the selected tab."""
match event.value:
case str(tabname):
destination = state.active_report_path if tabname == REPORTS_TAB else tabname
ui.navigate.to(destination)
case _:
return
with ui.column().classes("mx-auto"), ui.tabs() as tabs:
ui.tab("/", label="Overview", icon="space_dashboard")
ui.tab("/projects", label="Projects", icon="folder_open")
ui.tab(REPORTS_TAB, label="Reports", icon="summarize")
ui.tab("/settings", label="Settings", icon="settings")
tabs.set_value(tab_name_for_route(initial_route))
tabs.on_value_change(navigate)
return tabs
def render_tab_panels(tabs: ui.tabs, state: NavigationState, active_tab: str) -> None:
"""Render all tabbed page content inside a tab panels container."""
with ui.tab_panels(tabs, value=active_tab, animated=True).classes("w-full"):
with ui.tab_panel("/"):
overview_page()
with ui.tab_panel("/projects"):
projects_page()
with ui.tab_panel(REPORTS_TAB):
report_page(state)
with ui.tab_panel("/settings"):
settings_page()
def root() -> None:
"""Build the persistent application shell and sub-page container."""
initial_route = normalize_route(ui.context.client.sub_pages_router.current_path)
state = NavigationState()
if tab_name_for_route(initial_route) == REPORTS_TAB:
state.active_report_path = initial_route
with ui.header(elevated=True).classes("py-0 items-center"):
tabs = create_tabs(state, initial_route)
ui.button(icon="settings").classes("text-white").props("round flat").tooltip("Settings")
render_tab_panels(tabs, state, tab_name_for_route(initial_route))
def route_overview() -> None:
tabs.set_value("/")
def route_projects() -> None:
tabs.set_value("/projects")
def route_reports(report_id: str) -> None:
state.active_report_path = f"/reports/{report_id}"
tabs.set_value(REPORTS_TAB)
def route_settings() -> None:
tabs.set_value("/settings")
routes: dict[str, PageBuilder] = {
"/": route_overview,
"/projects": route_projects,
"/reports/{report_id}": route_reports,
"/settings": route_settings,
}
ui.sub_pages(routes).classes("hidden")
if __name__ in {"__main__", "__mp_main__"}:
ui.run(root, title="Northstar", port=8888, reload=True)
@@ -0,0 +1,253 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "nicegui==3.16.0",
# ]
# ///
from nicegui import events
from nicegui import ui
type TableValue = str | int | float
type TableRow = dict[str, TableValue]
STATUS_COLORS = {
"Ready": "positive",
"Low": "warning",
"Backorder": "negative",
}
COLUMNS = [
{
"name": "actions",
"label": "Actions",
"required": True,
"align": "center",
},
{
"name": "name",
"label": "Product",
"field": "name",
"required": True,
"sortable": True,
"align": "left",
"headerClasses": "bg-grey-2 text-grey-9 font-bold",
"classes": "font-medium",
"headerStyle": "width: 40%; min-width: 12rem",
"style": "width: 40%; min-width: 12rem",
},
{
"name": "category",
"label": "Category",
"field": "category",
"sortable": True,
"align": "left",
"headerStyle": "width: 9rem",
"style": "width: 9rem",
},
{
"name": "stock",
"label": "In stock",
"field": "stock",
"sortable": True,
"align": "right",
"headerStyle": "width: 7rem",
"style": "width: 7rem",
":classes": "row => row.stock < 10 ? 'text-negative font-bold' : ''",
":format": "value => value == null ? '' : `${value} units`",
},
{
"name": "price",
"label": "Unit price",
"field": "price",
"sortable": True,
"align": "right",
"headerStyle": "width: 8rem",
"style": "width: 8rem",
":format": "value => value == null ? '' : `$${value.toFixed(2)}`",
},
{
"name": "status",
"label": "Status",
"field": "status",
"sortable": True,
"align": "center",
"headerStyle": "width: 8rem",
"style": "width: 8rem",
"colorByValue": STATUS_COLORS,
},
{
"name": "updated_at",
"label": "Updated",
"field": "updated_at",
"sortable": True,
"align": "left",
"headerStyle": "width: 13rem",
"style": "width: 13rem",
":sort": "(left, right) => Date.parse(left) - Date.parse(right)",
":format": """(() => {
const formatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'UTC',
});
return value => {
if (!value) return '';
const timestamp = Date.parse(value);
return Number.isNaN(timestamp) ? 'Invalid date' : formatter.format(timestamp);
};
})()""",
},
]
ROWS: list[TableRow] = [
{
"id": 101,
"name": "Desk lamp",
"category": "Lighting",
"stock": 7,
"price": 42.5,
"status": "Low",
"updated_at": "2026-08-31T16:20:00Z",
},
{
"id": 102,
"name": "Task chair",
"category": "Seating",
"stock": 18,
"price": 289.0,
"status": "Ready",
"updated_at": "2026-09-01T08:45:00Z",
},
{
"id": 103,
"name": "Monitor arm",
"category": "Hardware",
"stock": 0,
"price": 119.95,
"status": "Backorder",
"updated_at": "2026-08-29T11:05:00Z",
},
{
"id": 104,
"name": "Cable tray",
"category": "Hardware",
"stock": 34,
"price": 31.25,
"status": "Ready",
"updated_at": "2026-09-01T14:30:00Z",
},
{
"id": 105,
"name": "Side table",
"category": "Furniture",
"stock": 9,
"price": 164.5,
"status": "Low",
"updated_at": "2026-08-30T19:15:00Z",
},
{
"id": 106,
"name": "Floor light",
"category": "Lighting",
"stock": 15,
"price": 98.0,
"status": "Ready",
"updated_at": "2026-09-01T10:10:00Z",
},
]
def render_table() -> ui.table:
table = ui.table(
columns=COLUMNS,
column_defaults={"headerClasses": "text-grey-8"},
rows=ROWS,
row_key="id",
pagination={
"sortBy": "stock",
"descending": True,
"rowsPerPage": 5,
},
).classes("w-full max-w-5xl")
(
table.props(
# Surface and cell layout.
"flat bordered separator=horizontal wrap-cells"
)
.props(
# Local sorting and page-size choices; zero means "all rows".
'binary-state-sort :rows-per-page-options="[5, 10, 0]"'
)
.props(
# Compact cells only below Quasar's medium breakpoint.
':dense="Quasar.Screen.lt.md"'
)
.props(
# Distinguish an empty dataset from a filter with no matches.
'no-data-label="No inventory items" no-results-label="No matching inventory items"'
)
)
with table.add_slot("top-left"), ui.row(align_items="center").classes("gap-2"):
ui.icon("inventory_2", size="sm", color="primary")
ui.label("Inventory").classes("text-xl font-medium")
ui.badge(str(len(ROWS)), color="grey-3", text_color="grey-9")
with table.add_slot("top-right"):
ui.input(placeholder="Search inventory").props("dense outlined clearable debounce=250").bind_value_to(
table,
"filter",
)
with table.add_slot("body-cell-status"), table.cell("status"):
ui.badge(outline=True).props(':label="props.value" :color="props.col.colorByValue[props.value] ?? \'grey\'"')
def open_product(event: events.GenericEventArguments) -> None:
product = next((row for row in table.rows if row[table.row_key] == event.args), None)
if product is None:
ui.notify("Product no longer exists", type="negative")
return
ui.notify(f"Opening {product['name']}")
with (
table.add_slot("body-cell-actions"),
table.cell("actions"),
ui.button(icon="open_in_new")
.props("round flat size='sm'")
.on(
"click.stop",
handler=open_product,
js_handler="() => emit(props.key)",
),
):
ui.tooltip("Open product")
with table.add_slot("no-data"), ui.row(align_items="center").classes("w-full justify-center gap-2 p-6 text-grey-7"):
ui.icon("inventory_2", size="2em").props(":name=\"props.filter ? 'filter_alt_off' : 'inventory_2'\"")
ui.element("span").props(':textContent="props.message"')
optional_columns = [column for column in table.columns if not column.get("required")]
def set_visible_columns(names: list[str]) -> None:
table.props["visible-columns"] = names
table.update()
ui.select(
{column["name"]: column["label"] for column in optional_columns},
value=[column["name"] for column in optional_columns],
label="Visible columns",
multiple=True,
clearable=True,
on_change=lambda event: set_visible_columns(event.value),
).props("outlined dense options-dense").classes("w-64")
return table
if __name__ in {"__main__", "__mp_main__"}:
with ui.column(align_items="center").classes("w-full gap-4 p-4"):
render_table()
ui.run(port=8888, reload=True)
@@ -0,0 +1,236 @@
# NiceGUI Application Architecture
Load this reference for application composition, package boundaries, and optional subsystem decisions.
## Baseline Package Boundaries
- `main.py`: process entry point and app factory exposure.
- `bootstrap.py`: app composition, router wiring, page registration, and lifespan orchestration.
- `config.py`: typed settings and environment parsing.
- `logging.py`: centralized logging setup.
- `api/`: HTTP transport that delegates to services.
- `services/`: business and use-case logic.
- `ui/pages/`: route-level NiceGUI pages.
- `ui/components/`: shared presentation building blocks.
Recommended base shape:
```text
.
├─ pyproject.toml
├─ .env.example
├─ src/
│ └─ app/
│ ├─ __init__.py
│ ├─ main.py
│ ├─ config.py
│ ├─ logging.py
│ ├─ api/
│ │ ├─ __init__.py
│ │ └─ health.py
│ ├─ services/
│ │ ├─ __init__.py
│ │ └─ example_service.py
│ └─ ui/
│ ├─ __init__.py
│ ├─ components/
│ │ ├─ __init__.py
│ │ └─ nav.py
│ └─ pages/
│ ├─ __init__.py
│ ├─ home.py
│ ├─ dashboard.py
│ └─ about.py
└─ tests/
├─ test_health.py
└─ test_pages_registration.py
```
## Required Baseline Behavior
- FastAPI is the base ASGI app.
- `create_app()` composes routes, resources, and NiceGUI.
- Lifespan owns startup and shutdown resources.
- NiceGUI pages are modular and explicitly registered.
- FastAPI exposes a health route such as `/healthz`.
- Imports do not trigger runtime global side effects.
For the ownership relationship between a caller-created FastAPI app, `nicegui.app`, `ui.run_with()`, Uvicorn, and a packaged startup command, load [FastAPI and Uvicorn startup](./fastapi-uvicorn-startup.md).
## Dependency Direction
Prefer:
- `main/bootstrap` -> `config/logging` + `api` + `ui/pages` + `services`
- `api` -> `services`
- `ui/pages` -> `ui/components` + `services`
- `services` -> helpers, clients, and `db/` when enabled
Avoid imports from services back into API or UI modules.
## Page And Component Ownership
Page modules should be a thin route-level composition layer. A page resolves route inputs and page-scoped dependencies, establishes the page shell, composes reusable components, and wires only the interactions that cross component boundaries. It should not contain a component's internal element tree, field bindings, refresh logic, domain rules, persistence, or long-running synchronous work.
Extract a presentation pattern to `ui/components/` when it appears on two or more pages or owns a meaningful state or interaction boundary. Reusable components should accept initial data, use-case functions, and event callbacks explicitly instead of importing page state or business services implicitly.
For page composition, responsive layout, Quasar props, and CSS customization, load [styling and customization](./styling-and-customization.md).
## Reusable Component Contract
In this architecture, a "component" is an application-level composition pattern, not necessarily a custom Vue component or a subclass of NiceGUI `Element`. Its usual shape is:
1. A typed dataclass represents the component's public handle and local UI state.
2. A render or factory function creates one component instance, builds its element subtree, and binds elements to that instance.
3. The function returns the instance so its caller can read or change intentional state, invoke public actions, or coordinate it with another component.
4. Internal elements, event handlers, validation feedback, and refreshable regions remain private to the component unless an imperative element handle is intentionally part of its API.
Use [`binding.bindable_dataclass`](./binding-dataclasses.md) for fields that drive or receive element properties. Plain `@dataclass` is sufficient when the returned object only groups element handles or callbacks and does not need immediate field propagation. Use `bindable_fields` when the dataclass also stores injected dependencies or other fields that should not participate in NiceGUI's binding graph.
```python
from collections.abc import Awaitable, Callable
from dataclasses import field
from nicegui import binding, ui
Search = Callable[[str], Awaitable[list[str]]]
@binding.bindable_dataclass(bindable_fields={"query", "busy", "items"})
class SearchPanel:
search: Search = field(repr=False)
query: str = ""
busy: bool = False
items: list[str] = field(default_factory=list)
@ui.refreshable_method
def render_results(self) -> None:
if not self.items:
ui.label("No results")
for item in self.items:
ui.label(item)
async def submit(self) -> None:
if self.busy:
return
self.busy = True
try:
self.items = await self.search(self.query)
await self.render_results.refresh()
finally:
self.busy = False
def render_search_panel(search: Search) -> SearchPanel:
panel = SearchPanel(search=search)
with ui.column().classes("w-full gap-3"):
ui.input("Search").bind_value(panel, "query")
ui.button("Search", on_click=panel.submit).bind_enabled_from(
panel,
"busy",
backward=lambda busy: not busy,
)
ui.label().bind_text_from(
panel,
"items",
backward=lambda items: f"{len(items)} results",
)
panel.render_results()
return panel
```
The returned dataclass is the component API. Its bindable fields synchronize stable element properties, while `render_results()` owns a bounded region whose child structure changes with `items`. The injected `search` callable preserves dependency direction: the component can invoke a use case without locating a service globally.
[`@ui.refreshable_method`](https://nicegui.io/documentation/refreshable) is the instance-oriented refresh surface for this pattern. NiceGUI records refresh targets by method instance, allowing each page-created component object to refresh independently. Detailed target, argument, async, and lifecycle behavior is documented under [refreshable component regions](./interaction-patterns.md#refreshable-component-regions).
### Thin Page Example
```python
from nicegui import ui
from app.services.catalog import search_catalog
from app.ui.components.search_panel import render_search_panel
@ui.page("/catalog")
def catalog_page() -> None:
with ui.column().classes("mx-auto w-full max-w-5xl gap-6"):
ui.label("Catalog").classes("text-2xl font-semibold")
render_search_panel(search_catalog)
```
The page owns the route and composition. The component owns its controls, binding graph, feedback state, and structural refresh. The service owns search rules and data access. If two component handles must coordinate, keep the page wiring declarative, such as subscribing one component's public event to another component's public refresh action; move orchestration with domain meaning into a service.
### Component Lifetime
Create component state during each page build unless sharing is deliberate. A module-global component dataclass can leak UI state across clients, and a module-global `@ui.refreshable` function can refresh every recorded target. Do not retain returned handles beyond their owning client without an explicit cleanup and stale-client policy.
Bindings to elements are removed with NiceGUI's element lifecycle. Refreshing a region deletes and recreates the elements inside that region, so external code should retain the component handle rather than private child element references. Component-owned subscriptions, timers, and background tasks must follow the client deletion rules in [interaction mechanics](./interaction-patterns.md#page-and-client-lifetime).
## Optional Persistence
Use only when the product requires durable data.
```text
src/app/db/
├─ __init__.py
├─ base.py
├─ session.py
├─ models/
└─ repositories/
```
- Create one engine and sessionmaker per process.
- Provide request- or operation-scoped sessions with `yield`.
- Keep transaction boundaries explicit in service or repository flows.
- Never share sessions across concurrent tasks.
- Use Alembic as the schema migration source of truth.
## Optional LangGraph AI
Use only for multi-step orchestration, resumable work, streaming, or human approval.
```text
src/app/ai/
├─ state.py
├─ nodes/
├─ graphs/
├─ runtime.py
└─ contracts.py
```
- Keep graph internals outside API and UI modules.
- Invoke graphs through a service such as `services/ai_service.py`.
- Use stable thread or session IDs for resumable flows.
- Keep interrupt payloads JSON-serializable.
## Optional Mounted Docs
Use only when generated docs must be served by the application.
Suggested settings:
- `docs_enabled`
- `docs_mount_path`
- `docs_site_dir`
- `docs_require_build`
Mount docs in the composition layer, normalize the mount path, avoid route conflicts, and define behavior for missing build artifacts.
## Async And Responsiveness
- Use `async def` where a handler or service path performs I/O.
- Prefer non-blocking clients and libraries.
- Offload CPU-heavy work to worker or background execution.
- Define progress, cancellation, timeout, completion, and error states for long actions.
- Stream or chunk results when workflows are long-running or multi-step.
## Testing Minimums
- Test the FastAPI health route.
- Test page registration wiring.
- If persistence is enabled, test session lifecycle and rollback behavior.
- If AI is enabled, test happy paths and interrupt/resume behavior.
- If docs are enabled, test the mounted index route.
- For long actions, test loading, completion, and error states.
@@ -0,0 +1,299 @@
# Binding Dataclasses
Use this reference to understand how NiceGUI creates binding links, detects changes, propagates values, and applies `forward` and `backward` transforms.
The implementation details and signatures below are verified against NiceGUI `3.16.0`. Check the target project's pinned version before copying version-sensitive behavior.
## Primary Sources
- [NiceGUI binding documentation](https://www.nicegui.io/documentation/section_binding_properties): public binding behavior and examples
- [NiceGUI `binding.py` at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/binding.py): binding graph, propagation, active links, strict checks, and `bindable_dataclass`
- [NiceGUI `ValueElement` at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/mixins/value_element.py): `bind_value*` signatures and transform direction
- [Python dataclasses](https://docs.python.org/3/library/dataclasses.html): generated methods, fields, defaults, and mutable-value rules
- [PEP 557](https://peps.python.org/pep-0557/): dataclass design rationale
## What `bindable_dataclass` Changes
`@binding.bindable_dataclass` first applies Python's `@dataclass`, then replaces each selected field on the resulting class with a NiceGUI `BindableProperty` descriptor. The descriptor stores the field value privately and intercepts later assignment.
```python
from nicegui import binding, ui
@binding.bindable_dataclass
class Profile:
name: str = "Ada"
age: int = 37
profile = Profile()
ui.input("Name").bind_value(profile, "name")
ui.number("Age", min=0).bind_value(profile, "age")
ui.label().bind_text_from(
profile,
"name",
backward=lambda name: f"User: {name}",
)
```
Assigning a different value to `profile.name` invokes the descriptor immediately. It records the new value, propagates it through the binding graph, and then runs any descriptor change handler. Assigning an equal value returns without propagation.
By default every dataclass field is bindable. Pass `bindable_fields` to limit descriptor conversion:
```python
@binding.bindable_dataclass(bindable_fields={"query", "page_size"})
class SearchState:
query: str = ""
page_size: int = 25
request_count: int = 0
```
A bound field omitted from `bindable_fields` still works, but NiceGUI must treat it as an active link and poll it for changes.
## Bindable Dataclasses As Component Handles
A reusable NiceGUI component can expose a bindable dataclass as its typed public handle. The component's render function creates the dataclass instance, builds the element subtree, establishes bindings against that instance, and returns it to the page. This keeps the page at the composition level while the component owns its field wiring and internal elements. See the complete [reusable component contract](./architecture.md#reusable-component-contract).
Choose binding direction according to what the public field represents:
| Component field | Typical element relationship | Binding surface |
| --- | --- | --- |
| editable value such as `query` | control and component share the value | `element.bind_value(handle, "query")` |
| rendered status such as `busy` or `count` | component state drives text, visibility, or enabled state | `bind_*_from` with a pure transform when needed |
| browser proposal requiring validation | event handler validates before assignment | explicit callback, then assign the accepted bindable field |
| collection controlling child count or layout | component state is read while rebuilding a bounded subtree | `@ui.refreshable_method`, not a binding to the child list itself |
| injected service or callback | component implementation dependency | ordinary dataclass field omitted from `bindable_fields` |
The returned handle should expose intentional component state and actions, not every child element. Bindable fields are effective for stable element properties because assignments propagate immediately. They do not create or delete elements when collection structure changes; a component-owned refreshable method should rebuild that region after the authoritative field is replaced. The target and instance behavior is defined under [refreshable component regions](./interaction-patterns.md#refreshable-component-regions).
Create one handle during each page build unless shared state is deliberate. A module-global bindable component model propagates across clients that bind to it, just as a module-level refreshable function can own targets from multiple clients.
## Binding Graph And Propagation
NiceGUI stores bindings as directed edges from one object attribute to another. A two-way binding is two one-way edges with transforms in opposite directions.
When an edge is registered, NiceGUI propagates its source immediately. For a two-way binding, it registers and runs the `backward` edge first, then registers the `forward` edge. The model value therefore wins initial synchronization and seeds the control.
After registration, propagation follows these rules:
1. A `BindableProperty` assignment starts propagation immediately when `old_value != new_value`.
2. NiceGUI walks outgoing edges depth first.
3. Each object-and-attribute node is visited at most once during that propagation pass, preventing a two-way cycle from running forever.
4. Each edge transforms the source value, compares it with the target, and only assigns and continues when the values differ.
Since NiceGUI `2.16.0`, this depth-first walk updates each affected node once per pass. Transform functions must not depend on call count or traversal order.
## Authoritative Models And Projections
A bindable dataclass can own canonical page state while plain dictionaries or component properties act as serializable projections. Use a one-way binding from each model field to its projection when browser rendering requires a different container shape:
```python
from nicegui import binding
projection = {"name": profile.name}
binding.bind_to(
profile,
"name",
projection,
"name",
other_strict=True,
)
```
Assigning `profile.name` then propagates immediately to `projection["name"]`. The projection is transport state, not a second business model; application code should locate and mutate the owning dataclass rather than treating browser-visible dictionaries as authoritative. This distinction is especially useful when one client-side scoped template renders many records and therefore cannot bind to one fixed Python object. The [editable-table pattern](./tables.md) applies it to one row dataclass and one QTable payload per stable row identity.
Browser-originated values still require Python validation before model assignment. Keep editable fields explicit, normalize into domain types, verify permissions and record existence, and only then assign the bindable field. For the client event path that carries such proposals, see [server-authoritative edit proposals](./component-mechanics.md#server-authoritative-edit-proposals).
### Persistence And Rollback
Treat a dataframe, service, or repository as the persistence boundary around the canonical bindable model:
1. validate and normalize the proposed value
2. remember the previous model value
3. assign the normalized value so bound projections update
4. persist the model through the owning adapter, service, or repository
5. if persistence fails, restore the previous model value before reporting or re-raising the error
6. refresh the affected component from the resulting projection on both acceptance and rejection
For asynchronous persistence, await the transaction and refresh only after it commits or rolls back. Catch expected validation, conflict, and persistence exceptions separately so the interface can report actionable failures without hiding programming errors. Component-specific refresh APIs and identity rules remain the responsibility of the consuming pattern; for QTable, see [persistence and row refresh](./tables.md#persistence-and-row-refresh).
## Bindable Properties Versus Active Links
| Source | Change detection | Update timing |
| --- | --- | --- |
| NiceGUI element property or `BindableProperty` field | descriptor intercepts assignment | immediate |
| ordinary object attribute or mapping entry | refresh loop compares source and target | next refresh step |
| tuple path such as `("address", "city")` | the full path is not a single bindable descriptor key | refresh loop unless the owning leaf object is bound directly |
The active-link refresh interval defaults to `0.1` seconds and is configured with `binding_refresh_interval` in `ui.run(...)`. Every refresh applies the transform and compares the result, so polling large collections or running expensive transforms can block the event loop. Tune the interval only after measuring; first reduce active links and transform cost.
## Transform Direction
The names `forward` and `backward` are relative to the element on which `bind_value*` is called:
| API | Source to target | Transform |
| --- | --- | --- |
| `element.bind_value_to(model, "field")` | element to model | `forward` |
| `element.bind_value_from(model, "field")` | model to element | `backward` |
| `element.bind_value(model, "field")` | both directions | both; `backward` runs first initially |
Each transform adapts the source value before NiceGUI assigns it to the target. The examples below convert between control values and native Python types only to make the two directions easy to observe; they do not prescribe a state-modeling approach.
Keep both functions pure, fast, and valid for every value the source can emit. NiceGUI does not turn transform exceptions into validation messages.
## Example: Observe Both Directions
This example uses [`datetime.date`](https://docs.python.org/3/library/datetime.html#date-objects) and `int` conversions to expose the mechanics. Their different representations make it clear which transform runs as a value crosses each binding edge.
```python
from dataclasses import field
from datetime import date
from nicegui import binding, ui
@binding.bindable_dataclass
class ReportFilters:
start_on: date = field(default_factory=date.today)
page_size: int = 25
filters = ReportFilters()
ui.date().bind_value(
filters,
"start_on",
forward=date.fromisoformat, # control str -> model date
backward=date.isoformat, # model date -> control str
)
ui.select(
options={"10": "10 rows", "25": "25 rows", "50": "50 rows"},
label="Page size",
).bind_value(
filters,
"page_size",
forward=int, # control str -> model int
backward=str, # model int -> control str
)
ui.label().bind_text_from(
filters,
"start_on",
backward=lambda value: f"Starting {value:%d %B %Y}",
)
```
At binding time, NiceGUI runs `backward` from the model to each control. Later control changes run `forward` toward the model. Assigning a new model value runs `backward` again.
## Example: Follow A Constrained Value
A select and an [`Enum`](https://docs.python.org/3/library/enum.html) provide a second visible representation change. Because the select only emits known values, this example keeps attention on propagation rather than parse failures.
```python
from enum import Enum
from nicegui import binding, ui
class SortOrder(Enum):
NEWEST = "newest"
OLDEST = "oldest"
@binding.bindable_dataclass
class ResultsState:
sort_order: SortOrder = SortOrder.NEWEST
state = ResultsState()
ui.select(
options={"newest": "Newest first", "oldest": "Oldest first"},
label="Sort order",
).bind_value(
state,
"sort_order",
forward=SortOrder, # control str -> model SortOrder
backward=lambda value: value.value, # model SortOrder -> control str
)
```
The concrete types are incidental. The same graph mechanics apply whenever `forward` and `backward` map two representations.
## Dataclass Modeling Rules
- Use `field(default_factory=...)` for mutable defaults and time-dependent defaults.
- NiceGUI `3.16.0` rejects `frozen=True` and `slots=True` in `bindable_dataclass`; both conflict with its descriptor storage model.
- Keep UI-editable fields explicit and typed. Dataclass annotations describe intent but do not enforce runtime types; the control or transform must produce the right type.
- Replace collections instead of mutating them in place.
```python
from dataclasses import field
from nicegui import binding
@binding.bindable_dataclass
class Filters:
query: str = ""
tags: list[str] = field(default_factory=list)
filters = Filters()
filters.tags = [*filters.tags, "python"] # unequal assignment propagates
```
Calling `filters.tags.append("python")` bypasses the descriptor. Mutating first and then assigning an equal copy also does not propagate because `BindableProperty` compares with `!=` and returns when values are equal.
## Nested Structures
Tuple paths support nested mappings and object attributes:
```python
data = {"user": {"name": "Ada"}}
ui.input("Name").bind_value(data, ("user", "name"))
ui.label().bind_text_from(data, ("user", "name"))
```
A tuple path is checked as an active link. When a nested object is itself a bindable dataclass, bind its owning object directly to preserve immediate descriptor-driven propagation:
```python
ui.input("City").bind_value(profile.address, "city")
```
If `profile.address` is replaced later, rebuild that direct binding or bind through the root tuple path and accept active-link polling.
## Strictness And Missing Paths
NiceGUI `3.16.0` checks object attributes by default and does not check mapping keys by default. A failed strict check raises `AttributeError` or `KeyError` while the binding is being created.
```python
from nicegui import app, ui
ui.input().bind_value(app.storage.user, "display_name", strict=True)
```
Use `strict=False` for an intentionally lazy object attribute and `strict=True` when a mapping key must already exist. On assignment, NiceGUI can create missing intermediate dictionaries, but it cannot create missing intermediate object attributes.
## Common Pitfalls
- Do not put logging, I/O, model mutation, notifications, or other side effects in transforms. Propagation order and call count are implementation details.
- Do not use a transform as the validation boundary for free-form text. A raised parser exception interrupts propagation.
- Do not mutate a bound collection in place. Construct and assign a different value.
- Do not assume a nested tuple path gets the same immediate behavior as binding directly to a bindable leaf object.
- Scope bindable models to the appropriate page, client, or user. A module-global model shares state across users.
- Remove bindings with NiceGUI's public element lifecycle rather than retaining discarded elements or models indefinitely.
## Version Checks
- `bindable_dataclass` was added in NiceGUI `2.11.0`.
- Depth-first binding propagation changed in NiceGUI `2.16.0`.
- Binding strictness controls were added in NiceGUI `3.0.0`.
- Tuple paths for nested properties were added in NiceGUI `3.10.0`.
- NiceGUI `3.16.0` supports `bindable_fields` and rejects `slots=True` and `frozen=True`.
Verify the installed NiceGUI source and documentation when any of these mechanics affect application correctness.
@@ -0,0 +1,189 @@
# NiceGUI And Quasar Color Theming
This reference describes how NiceGUI's Python color APIs map onto Quasar's browser-side color system. It distinguishes theme configuration from individual element colors, fixed palette colors from runtime brand roles, and palette values from dark-mode state.
The primary public references are [NiceGUI styling and appearance](https://nicegui.io/documentation/section_styling_appearance), [NiceGUI color theming](https://nicegui.io/documentation/colors), and the [Quasar color palette](https://quasar.dev/style/color-palette).
## Boundary At A Glance
NiceGUI does not define an independent component theme engine. It configures and consumes the Quasar color system while adding Python-facing scope, value classification, and CSS cascade behavior.
| Surface | NiceGUI owns | Quasar or the browser owns |
| --- | --- | --- |
| `app.colors(...)` | application-wide Python configuration and custom-name registration | initial Quasar brand configuration and the resulting `--q-*` values on each page |
| `ui.colors(...)` | a page-level element and precedence over `app.colors()` | runtime `--q-*` properties on `document.body` plus custom `text-*` and `bg-*` classes |
| component `color=` and `text_color=` arguments | classification of supported values as Quasar, Tailwind, or CSS colors on color-aware wrappers | rendering through a Quasar prop, a utility class, or an inline CSS declaration |
| `.props("color=...")` | transport of the prop to the frontend component | interpretation of the value by that Quasar component |
| `.classes("text-primary bg-positive")` | attachment of class names and NiceGUI's CSS layer arrangement | Quasar's semantic utility classes and their `--q-*` variable references |
| `ui.dark_mode(...)` | Python control and binding with `True`, `False`, or automatic `None` state | Quasar dark-mode state, `body--light` or `body--dark`, and dark-aware components |
The central handoff is a CSS custom property. NiceGUI supplies a value such as `#176b5b`; Quasar components and helpers consume `var(--q-primary)`.
## Quasar Color Namespaces
Quasar exposes two materially different kinds of color name. Only one kind is changed by NiceGUI's theme APIs.
### Runtime Brand Roles
Quasar's semantic brand roles are backed by root or body-level CSS custom properties. Components and semantic utility classes follow these values at runtime. NiceGUI exposes the eight Quasar brand roles and the separate dark-page surface through `app.colors()` and `ui.colors()`.
| NiceGUI argument | CSS custom property | NiceGUI default | Intended meaning |
| --- | --- | --- | --- |
| `primary` | `--q-primary` | `#5898d4` | main action and brand emphasis |
| `secondary` | `--q-secondary` | `#26a69a` | secondary brand emphasis |
| `accent` | `--q-accent` | `#9c27b0` | accent emphasis |
| `dark` | `--q-dark` | `#1d1d1d` | dark component surface |
| `dark_page` | `--q-dark-page` | `#121212` | dark page background |
| `positive` | `--q-positive` | `#21ba45` | success state |
| `negative` | `--q-negative` | `#c10015` | error or destructive state |
| `info` | `--q-info` | `#31ccec` | informational state |
| `warning` | `--q-warning` | `#f2c037` | warning state |
For example, `color="primary"`, `.props("color=primary")`, `text-primary`, and `bg-primary` all reach Quasar's semantic primary role. Changing that role changes every consumer of `--q-primary`; it does not rewrite fixed palette colors.
```python
from nicegui import app, ui
app.colors(
primary="#176b5b",
secondary="#52645f",
accent="#c05a32",
positive="#2e7d32",
negative="#b3261e",
info="#276b8e",
warning="#a86600",
dark="#202523",
dark_page="#151917",
)
ui.button("Save")
ui.label("Saved").classes("text-positive")
```
The current NiceGUI client implementation writes page-level values to `document.body` in [`colors.js`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/colors.js). Quasar's semantic helpers reference those properties, as described under [dynamic brand colors](https://quasar.dev/style/color-palette#dynamic-change-of-brand-colors-dynamic-theme-colors).
### Fixed Palette Colors
Names such as `red-5`, `teal-10`, and `blue-grey-2` belong to Quasar's compiled [color list](https://quasar.dev/style/color-palette#color-list). Their `text-*` and `bg-*` classes contain fixed color values rather than references to the semantic brand variables.
Consequently:
- `ui.colors(primary="#0057b8")` changes `primary`, `text-primary`, and `bg-primary` consumers.
- It does not change `blue`, `blue-6`, `text-blue-6`, or `bg-blue-6`.
- A fixed palette color can be assigned to a component, for example `ui.button("Open", color="teal-7")`, without adding it to the application theme.
The fixed palette is a Quasar facility bundled into NiceGUI. It is not generated by `app.colors()` or `ui.colors()`.
### Custom Semantic Names
Extra keyword arguments create application-specific names:
```python
from nicegui import app, ui
app.colors(brand="#176b5b", review_required="#a86600")
ui.button("Continue", color="brand")
ui.label("Review required").classes("text-review-required")
```
NiceGUI normalizes underscores in Python keyword names to hyphens in browser color names. For each custom name, the client-side [`applyColors`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/static/nicegui.js) helper creates:
- a `--q-<name>` property on `document.body`
- a `.text-<name>` class that reads that property
- a `.bg-<name>` class that reads that property
This automates the custom-class pattern shown in Quasar's [adding your own colors](https://quasar.dev/style/color-palette#adding-your-own-colors) reference. NiceGUI also registers the name in its Python-side Quasar color set so color-aware wrappers pass the value as a Quasar color prop. The name must therefore be declared with `app.colors()` or `ui.colors()` before a NiceGUI component first uses it; this ordering requirement is part of the [NiceGUI custom colors contract](https://nicegui.io/documentation/colors#custom_colors).
## Scope And Precedence
The effective palette has three levels:
| Level | Scope | Effect |
| --- | --- | --- |
| bundled Quasar values | every page | fallback values supplied by Quasar's CSS |
| `app.colors(...)` | all NiceGUI pages | populates NiceGUI's Quasar brand configuration before each client app starts |
| `ui.colors(...)` | current page | writes the core and custom properties on that page's `document.body` and takes precedence over app-wide values |
`app.colors()` is configuration, not a rendered UI element. NiceGUI stores its values in the application's Quasar configuration; see the current [`App.colors` implementation](https://github.com/zauberzeug/nicegui/blob/main/nicegui/app/app.py).
`ui.colors()` is rendered into a specific page. Its DOM placement in a row, card, or other container does not scope the palette to that subtree because its client component writes to `document.body`. A page with two calls therefore has one effective page palette, with the last mounted call determining the core values. Subtree-specific theming requires application CSS variables or directly scoped `--q-*` overrides, not nested `ui.colors()` elements.
The `ui.colors()` initializer supplies all nine core values. A call such as `ui.colors(primary="#555")` is therefore a complete core-palette assignment: unspecified roles resolve to NiceGUI's defaults rather than acting as a one-property patch over `app.colors()`. Pages that must retain customized app-wide secondary, status, or dark values should pass those values explicitly in the page override.
`app.colors()` was added in NiceGUI 3.6.0, while custom colors were added to `ui.colors()` in 2.2.0. Applications pinned to earlier NiceGUI releases need version-matched behavior from the [NiceGUI colors reference](https://nicegui.io/documentation/colors).
## Element Color Values
On elements implemented with NiceGUI's color mixins, a `color`, `text_color`, or corresponding setter value is classified in this order by [`color_elements.py`](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/mixins/color_elements.py):
| Input kind | Example | NiceGUI output | Theme response |
| --- | --- | --- | --- |
| Quasar semantic, fixed, or registered custom name | `primary`, `red-5`, `brand` | Quasar component color prop | semantic and custom names follow `--q-*`; fixed names do not |
| recognized Tailwind color | `red-500` | `bg-red-500` or `text-red-500` class | independent of the Quasar palette |
| other CSS color value | `#ff0000`, `rgb(255 0 0)`, `rebeccapurple` | inline `background-color` or `color` | independent of the Quasar palette |
| `None` | `None` | removes the managed color | falls back to component and cascade defaults |
This classification is a NiceGUI convenience, not a general Quasar rule. Passing `.props("color=#ff0000")` bypasses NiceGUI's color mixin and asks the Quasar component to interpret `#ff0000` as its `color` prop. Likewise, components that expose a raw Quasar color prop without using the mixin may accept only the values documented by that component. The specific NiceGUI constructor documentation remains authoritative for each element.
Quasar and Tailwind color classes share the same HTML class list but not the same namespace conventions. `text-red-5` is a Quasar fixed-palette helper; `text-red-500` is a Tailwind-compatible utility. Semantic names such as `text-primary` are Quasar helpers.
## Palette Values And Dark Mode Are Separate
The `dark` and `dark_page` arguments define colors; they do not enable dark mode. Mode state is controlled by [`ui.dark_mode()`](https://nicegui.io/documentation/dark_mode), the `dark` argument of `ui.run()`, or a page decorator. `ui.dark_mode()` takes precedence for its page and maps `None` to Quasar's automatic system-preference mode.
When dark mode is active, Quasar:
- applies `body--dark` instead of `body--light`
- uses the dark page background and dark-aware component behavior
- automatically enables the dark state of Quasar components that support a `dark` prop
These behaviors are defined by [Quasar dark mode](https://quasar.dev/style/dark-mode). Application-owned surfaces can key off the same body class and reuse Quasar variables:
```css
:root {
--app-surface: #ffffff;
--app-text: #202623;
}
.body--dark {
--app-surface: var(--q-dark);
--app-text: #eef3f0;
}
```
Changing `--q-dark` while the page remains in light mode changes consumers of the `dark` role but does not add `body--dark`. Enabling dark mode without designing application-specific text, border, and surface tokens does not automatically recolor arbitrary custom CSS.
## CSS Classes And Cascade
NiceGUI ships Quasar's color helpers, so `.classes("text-primary")` and `.classes("bg-warning")` can be attached directly to NiceGUI elements. Quasar defines these helpers with `!important`.
NiceGUI changes the cascade arrangement around the bundled Quasar CSS. Its [CSS layer reference](https://nicegui.io/documentation/section_styling_appearance#css_layers) explains how Quasar rules are split into layers so important Tailwind utilities or application rules in suitable layers can override them. This is a NiceGUI integration detail; the class names and color semantics still come from Quasar.
Direct CSS can consume the same semantic properties without a Quasar class:
```css
.app-focus-ring {
outline: 2px solid var(--q-primary);
}
```
Such CSS follows runtime palette changes because it reads the same property. A literal declaration such as `outline-color: #176b5b` does not.
## Source Index
!!! info "Primary sources"
- [NiceGUI styling and appearance](https://nicegui.io/documentation/section_styling_appearance)
- [NiceGUI color theming](https://nicegui.io/documentation/colors)
- [NiceGUI dark mode](https://nicegui.io/documentation/dark_mode)
- [Quasar color palette](https://quasar.dev/style/color-palette)
- [Quasar dark mode](https://quasar.dev/style/dark-mode)
- [Quasar theme builder](https://quasar.dev/style/theme-builder)
!!! info "Implementation references"
- [NiceGUI app-wide color configuration](https://github.com/zauberzeug/nicegui/blob/main/nicegui/app/app.py)
- [NiceGUI page color element](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/colors.py)
- [NiceGUI page color client component](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/colors.js)
- [NiceGUI custom color CSS generation](https://github.com/zauberzeug/nicegui/blob/main/nicegui/static/nicegui.js)
- [NiceGUI element color classification](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/mixins/color_elements.py)
- [NiceGUI color behavior tests](https://github.com/zauberzeug/nicegui/blob/main/tests/test_colors.py)
@@ -0,0 +1,543 @@
# NiceGUI Component Mechanics
NiceGUI components are Python objects that describe browser UI elements. A component constructor creates an element, constructor arguments configure its common behavior, and methods on the returned object expose styling, events, bindings, slots, and client-side capabilities.
This reference begins with those everyday component APIs, then describes the NiceGUI, Quasar, Vue, and browser layers beneath them. Page structure, typography, responsive composition, and scaling are covered separately in [styling and customization](./styling-and-customization.md).
## Basic Components
Components are created from the `ui` namespace. Layout components are context managers, so nested Python blocks describe the element hierarchy:
```python
from nicegui import ui
with ui.column().classes("gap-3"):
name = ui.input("Name", placeholder="Ada")
role = ui.select(
options={"admin": "Administrator", "reader": "Reader"},
value="reader",
label="Role",
).props("outlined dense")
ui.button("Save", on_click=lambda: ui.notify(f"Saved {name.value}"))
```
The [NiceGUI component documentation](https://nicegui.io/documentation) is the index of available `ui.*` constructors. Each component page documents its Python parameters, values, callbacks, methods, and examples. The implementation for each wrapper is available in the [NiceGUI element source tree](https://github.com/zauberzeug/nicegui/tree/main/nicegui/elements).
## Common Component Mechanics
Most NiceGUI elements inherit a common set of mechanics from `Element`; individual wrappers add component-specific properties and methods.
| Surface | What it represents | Source of supported values |
| --- | --- | --- |
| Constructor arguments | NiceGUI's typed, Python-facing API for initial content, values, callbacks, validation, and common behavior | the component's page in the [NiceGUI component documentation](https://nicegui.io/documentation) and its wrapper in the [NiceGUI element source tree](https://github.com/zauberzeug/nicegui/tree/main/nicegui/elements) |
| Properties such as `.value` and `.options` | Python-side component state maintained by a particular wrapper | the component documentation and wrapper source; these properties are not universal `Element` APIs |
| Wrapper methods such as `set_options()` | NiceGUI state transitions that normalize Python data and schedule a client update | the component documentation and wrapper source |
| `.props(...)` | Quasar component props, Vue bindings, or HTML attributes serialized onto the frontend element | the API section of the wrapped component in the [Quasar component documentation](https://quasar.dev/vue-components); [NiceGUI element customization](https://nicegui.io/documentation/element) defines the bridge syntax |
| `.classes(...)` | CSS class names attached to the element | [Tailwind's utility documentation](https://tailwindcss.com/docs) for Tailwind classes; Quasar's [breakpoint](https://quasar.dev/style/breakpoints), [spacing](https://quasar.dev/style/spacing), [visibility](https://quasar.dev/style/visibility), and [helper-class](https://quasar.dev/style/other-helper-classes) references for Quasar classes; or the application's own stylesheets for custom classes |
| `.style(...)` | Inline CSS declarations attached to the element | the [MDN CSS reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference) |
| Constructor callbacks and `.on(...)` | NiceGUI callbacks and forwarded browser or Quasar events | the component's NiceGUI page first, then the Events section of its Quasar API; [NiceGUI generic events](https://nicegui.io/documentation/generic_events) documents `.on(...)` |
| `on_*` methods | Named event conveniences implemented by a specific NiceGUI wrapper, such as `on_value_change` | the component documentation and wrapper source; there is no universal list that applies to every component |
| `bind_*` methods | synchronization between element properties and Python model properties | [NiceGUI binding documentation](https://nicegui.io/documentation/section_binding_properties) and the wrapper's documented bindable properties |
| `add_slot(...)` | content inserted into a Quasar or Vue named slot | the Slots and Scoped Slots sections of the wrapped component's Quasar API |
| `run_method(...)` | invocation of a public method on the client component | the Methods section of the wrapped component's Quasar API |
### Options And Values
`options` is component state rather than a universal styling mechanism. Components such as `ui.select`, `ui.radio`, `ui.toggle`, and `ui.table` define their own accepted option shapes and value semantics. For example, NiceGUI's [`ui.select` documentation](https://nicegui.io/documentation/select) describes list and dictionary options, while the [`Select` wrapper source](https://github.com/zauberzeug/nicegui/blob/main/nicegui/elements/select.py) shows how those Python values are normalized for Quasar.
Reading `element.options` accesses the wrapper's current Python-side options. Assigning or mutating options only changes browser state when the wrapper detects or sends an update. Component helpers such as `set_options()` encode that synchronization behavior and therefore belong to the wrapper's API rather than to Quasar's raw `options` prop.
### Props
`.props()` writes props onto the frontend component:
```python
ui.button("Archive").props("outline color=negative")
ui.select(["A", "B"]).props("dense options-dense")
```
For NiceGUI elements backed by Quasar, supported names and values come from the wrapped Quasar component's API. For example, the full [`QSelect` API](https://quasar.dev/vue-components/select#qselect-api) lists `dense`, `options-dense`, `popup-content-class`, events, slots, and methods. NiceGUI may already expose some of those features as typed constructor arguments or wrapper methods; the NiceGUI component page and source describe that higher-level behavior.
#### Property-String Format
NiceGUI's `.props()` string is parsed on the Python side by the tagged [`Props.parse()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/props.py). It accepts whitespace-delimited tokens in these forms:
| Form | Python-side result | Frontend meaning |
| --- | --- | --- |
| `dense` | `{"dense": True}` | a true boolean prop |
| `label=Chair` | `{"label": "Chair"}` | a static string prop |
| `offset=[8, 8]` | `{"offset": [8, 8]}` | a Python literal serialized as a value |
| `:label=someExpression` | `{":label": "someExpression"}` | a JavaScript expression evaluated in the browser |
Quoted strings and bracketed or braced literals are parsed with Python's `ast.literal_eval`; unquoted values remain strings. Quote an expression when it contains whitespace or characters outside NiceGUI's unquoted-value grammar, or assign it through `element.props[":name"]` to avoid the string parser. Regular HTML attributes can pass through the same mechanism where the rendered element supports them. The [NiceGUI element documentation](https://nicegui.io/documentation/element) defines the public bridge syntax.
#### Dynamic Props And Vue Bindings
The leading colon borrows Vue's [`v-bind` shorthand](https://vuejs.org/api/built-in-directives.html#v-bind), but NiceGUI elements are created with Vue's `h()` render function rather than compiled from a template. NiceGUI's tagged [`renderRecursively()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/static/nicegui.js) removes the colon, evaluates the value as JavaScript, and passes the result in the vnode's props object. For example:
```python
ui.badge().props(':color="window.innerWidth < 600 ? \'primary\' : \'grey\'"')
```
corresponds conceptually to this Vue template:
```vue
<q-badge :color="window.innerWidth < 600 ? 'primary' : 'grey'" />
```
The right-hand side is JavaScript, not Python. It may read browser globals, call functions, or construct arrays and objects, provided the receiving HTML element or Vue component accepts the resulting property. Inside a scoped slot, NiceGUI additionally makes that slot's current scope object available under the name `props`; outside a scoped slot, that name has no slot object to reference.
There is one important render-function distinction. Vue template syntax allows argument-less `v-bind="object"` to spread every key in an object. A literal `.props("v-bind=someObject")` token is not compiled as a directive by NiceGUI's render-function path and does not spread the object. Use a raw `add_slot(..., template=...)` Vue template when a slot contract requires whole-object binding, or bind the documented fields individually. Vue's [render-function reference](https://vuejs.org/guide/extras/render-function.html#creating-vnodes) defines the equivalent programmatic form as passing or spreading those keys in the object supplied to `h()`.
#### Controlled Values And Model Events
Vue component `v-model` expands to a value prop plus an update listener. For the common `modelValue` contract, that means `modelValue` and `update:modelValue`, as defined by the [Vue component `v-model` guide](https://vuejs.org/guide/components/v-model.html) and its tagged [compiler transform](https://github.com/vuejs/core/blob/v3.5.22/packages/compiler-core/src/transforms/vModel.ts).
NiceGUI can express a deliberately one-way controlled value with a dynamic prop and handle the corresponding proposal separately. For example, `ui.number` follows QInput's common `modelValue` contract:
```python
number_editor = ui.number()
number_editor.props(':model-value="props.value"').on(
"update:model-value",
handler=apply_proposal,
js_handler="(value) => emit(value)",
)
```
This pattern is useful inside a scoped slot or whenever Python must authorize a change before reasserting component state. The dynamic prop displays the current client-side projection; the listener sends an edit proposal to Python instead of assigning into the source object in JavaScript. Use an ordinary NiceGUI value binding when the wrapper's two-way value model already matches the requirement.
##### `ui.input` Wrapper Exception
In NiceGUI `3.16.0`, `ui.input` is a NiceGUI client wrapper around QInput rather than a direct QInput element. The tagged [`input.js` component](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/input.js) defines its controlled prop and event as `value` and `update:value`, not `model-value` and `update:model-value`. It also adds a static empty `value` prop. Remove that prop before adding a row-scoped dynamic value:
```python
name_input = ui.input().props(remove="value")
name_input.props(':value="props.value"').on(
"update:value",
handler=apply_proposal,
js_handler="(value) => emit(value)",
)
```
Using `:model-value="props.value"` leaves this wrapper's own `value` unchanged, so repeated text inputs in a scoped table slot render blank. `ui.number` is a direct QInput specialization and therefore uses `model-value` and `update:model-value` as shown above. Check each NiceGUI wrapper's `VALUE_PROP` and client component before assuming the underlying Quasar model contract is exposed unchanged.
An `update:model-value` listener receives the component's emitted model value, whose shape is component-specific. A custom listener also bypasses normalization that a wrapper's built-in value handler may perform. For example, the Quasar input beneath `ui.number` can emit numeric text, so the Python proposal handler must perform authoritative numeric conversion.
NiceGUI serializes `ui.select` options into QSelect objects shaped like `{value: index, label: option_label}` and normally maps the selected object back to the corresponding Python option. A custom `js_handler` receives that object before NiceGUI's Python-side conversion. When list values and labels are intentionally identical, forward `option.label`; otherwise emit the index and resolve it against the authoritative Python options rather than trusting a browser-supplied label.
### Classes And Styles
`.classes()` adds class names to the rendered element:
```python
ui.label("Account").classes("text-lg font-semibold text-slate-800")
ui.row().classes("w-full items-center gap-4")
```
NiceGUI includes Tailwind-compatible utility styling, so names such as `flex`, `gap-4`, `w-full`, and `text-slate-800` are defined by Tailwind. The complete categorized list is the [Tailwind CSS documentation](https://tailwindcss.com/docs); its [utility-class guide](https://tailwindcss.com/docs/styling-with-utility-classes) explains variants, responsive prefixes, and arbitrary values. NiceGUI can alternatively run with a selected UnoCSS preset, whose compatibility limits are documented under [NiceGUI's UnoCSS engine](https://nicegui.io/documentation/section_styling_appearance#unocss_engine).
Quasar publishes its classes by category rather than through a single style index. The [breakpoint reference](https://quasar.dev/style/breakpoints) defines viewport thresholds, the [spacing reference](https://quasar.dev/style/spacing) lists the `q-p*` and `q-m*` permutations, the [visibility reference](https://quasar.dev/style/visibility) covers responsive and platform visibility, and the [other helper classes reference](https://quasar.dev/style/other-helper-classes) covers pointer, scrolling, sizing, rotation, and border helpers. Application-defined class names are supported when their CSS is loaded with `ui.add_css`, static assets, or page head content. `.style()` accepts CSS declarations directly, separated by semicolons.
### Events And `on_*` Methods
Callbacks supplied by a constructor are NiceGUI's documented event surface:
```python
ui.input("Search", on_change=lambda event: print(event.value))
ui.button("Refresh", on_click=lambda: print("refresh"))
```
Some wrappers also expose named registration methods such as `on_value_change`. Their availability and event argument type are component-specific and are documented on the NiceGUI component page or in its wrapper source.
`.on()` is the generic event bridge for events without a dedicated Python convenience API:
```python
field = ui.select(["A", "B"])
field.on("popup-show", lambda: print("opened"))
```
For Quasar-backed elements, the component API's Events section is the authoritative list of emitted event names and payloads. Native browser events are documented in the [MDN event reference](https://developer.mozilla.org/en-US/docs/Web/Events). NiceGUI's [generic event documentation](https://nicegui.io/documentation/generic_events) defines the public `.on()` API.
#### Mapping Quasar Event Names
Quasar documents each component event under its **Events** API entry. Use the documented kebab-case name with `.on()`. NiceGUI's tagged [`event_type_to_camel_case()` helper](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/helpers/strings.py) converts the event name before the first modifier dot to the camelCase form emitted by the component; its frontend renderer then creates Vue's `onXxx` listener prop. These forms therefore map to the same event path:
| Quasar API name | NiceGUI registration | Vue runtime listener |
| --- | --- | --- |
| `popup-show` | `.on("popup-show", ...)` | `onPopupShow` |
| `input-value` | `.on("input-value", ...)` | `onInputValue` |
| `update:model-value` | `.on("update:model-value", ...)` | `onUpdate:modelValue` |
Vue component events are notifications emitted by the direct component; unlike DOM events, they do not bubble through component ancestors. Prefer a NiceGUI constructor callback, binding, or named wrapper method when one already owns the same behavior. In particular, use `on_change` or a value binding instead of registering another `update:model-value` listener unless the lower-level model event is specifically required.
#### Reading Event Payloads
The Quasar event's documented `params` define the positional arguments received by the listener. NiceGUI serializes those arguments and exposes them as `GenericEventArguments.args` in Python. If exactly one argument is emitted, NiceGUI presents that value directly; multiple emitted arguments remain a list in their documented order.
For example, the version-matched [`QSelect` event API](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/select/QSelect.json) defines `add` as one details object containing `index` and `value`:
```python
def handle_add(event) -> None:
print(event.args["index"], event.args["value"])
item_select.on("add", handle_add, args=["index", "value"])
```
The `args` parameter controls transport, not Quasar's event signature:
| `args` value | Data sent to Python |
| --- | --- |
| `None` | all JSON-serializable attributes of every emitted argument |
| `[]` | no event arguments |
| `["index", "value"]` | only those attributes from a one-object event argument |
| `[[], ["name"], None]` | for a three-argument event: none from the first, `name` from the second, and all of the third |
Primitive values and arrays are forwarded as values rather than filtered by attribute name. Browser objects, DOM nodes, component references, functions, and cyclic structures are not meaningful server payloads; select the small serializable subset the Python handler actually needs.
#### Transforming Events In The Browser
`js_handler` receives the original Quasar or browser event arguments in the browser. Calling NiceGUI's injected `emit(...)` forwards only the transformed arguments to the Python `handler`:
```python
item_select.on(
"add",
handler=lambda event: print(event.args),
js_handler="(details) => emit({index: details.index, value: details.value})",
)
```
Omit the Python handler for a client-only action, or omit `js_handler` to use NiceGUI's default `(...args) => emit(...args)` forwarding behavior. Since NiceGUI `2.18.0`, both can be supplied together. A `js_handler` may also decide not to call `emit`, in which case no Python callback runs for that occurrence.
Events that pass imperative JavaScript callbacks require special care. For example, QSelect's `filter` event emits an input string plus `doneFn` and `abortFn` functions. Those functions cannot be serialized for later use by Python. Use NiceGUI's wrapper-supported filtering API, or consume such callbacks synchronously in browser-side JavaScript; do not treat them as ordinary server payloads.
#### Server-Authoritative Edit Proposals
Treat values received from the browser as proposals, even when Quasar validation or input constraints have already run. Attach the listener to the component that emits the event, use `js_handler` to send only the identity and serializable values Python needs, and validate the field allowlist, types, ranges, permissions, record existence, and persistence constraints in Python. The browser may keep temporary editor state, but it is not the source of truth.
Choose when proposals cross the client-server boundary according to the interaction:
- Use `update:model-value` for discrete editors such as selects, switches, and checkboxes.
- For text and numeric inputs accepted during typing, use the component's documented `debounce` prop to avoid a server round trip for every keystroke.
- For an explicit save/cancel workflow, keep a local draft in a dialog or popup and emit one proposal on save.
- During asynchronous persistence, disable the editor or expose a busy state. Add an entity version or another optimistic-concurrency check when multiple clients can edit the same record.
After validation, pass accepted values to the authoritative model and persistence boundary. See [bindable dataclasses](./binding-dataclasses.md#authoritative-models-and-projections) for projection, rollback, and refresh mechanics, and [editable tables](./tables.md) for the QTable-specific form of this pattern.
#### Modifiers And High-Frequency Events
Dot suffixes use Vue's [event and key modifier rules](https://vuejs.org/guide/essentials/event-handling.html#event-modifiers):
```python
field.on("keydown.enter", submit)
field.on("click.stop", handle_click)
viewport.on("scroll.passive", handle_scroll, throttle=0.1)
```
NiceGUI separates listener options such as `capture`, `once`, and `passive`, event modifiers such as `stop`, `prevent`, and `self`, and key filters such as `enter`. The tagged [`EventListener.to_dict()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/event_listener.py) performs that classification before the frontend applies Vue's `withModifiers()` and `withKeys()` helpers. `throttle`, `leading_events`, and `trailing_events` regulate messages sent to Python; they do not throttle a client-only `js_handler` that never calls `emit`.
### Custom Vue Components
When NiceGUI's wrappers and the documented Quasar extension points cannot express a component, subclass `ui.element` and pair it with a Vue component. Start from NiceGUI's [custom Vue component example](https://github.com/zauberzeug/nicegui/tree/main/examples/custom_vue_component), keeping Python responsible for the server-facing state and event contract.
For a component with npm dependencies, bundle the frontend module and pass its ESM module name and bundled file path through the `esm` parameter on the Python element subclass. NiceGUI adds that module to the page import map. The [signature pad example](https://github.com/zauberzeug/nicegui/tree/main/examples/signature_pad) and [node module integration example](https://github.com/zauberzeug/nicegui/tree/main/examples/node_module_integration) demonstrate the package and bundling boundary.
Treat the generated JavaScript and CSS as package data in executable builds. PyInstaller or Nuitka configuration must include those assets, and the packaged artifact must be checked for successful module loading rather than only for process startup. Do not introduce a custom Vue component merely to avoid a supported NiceGUI constructor, Quasar prop, event, slot, or public method.
## Framework Boundary Model
A NiceGUI component is not a Python-rendered HTML fragment. Customization passes through several owners:
| Layer | Owns | Inspect when |
| --- | --- | --- |
| NiceGUI Python wrapper | constructor arguments, Python value normalization, validation, bindings, event callbacks, and update helpers | behavior may already have a typed Python API or wrapper-specific state rules |
| NiceGUI element bridge | serialized props, classes, styles, events, slots, and frontend method calls | mapping a supported Vue or Quasar feature through NiceGUI |
| Quasar Vue component | documented props, emitted events, named slots, public methods, popup behavior, accessibility, and internal state | the NiceGUI constructor does not expose a required component feature |
| Vue and browser runtime | reactivity, rendered DOM, teleported content, CSS cascade, fonts, and static assets | diagnosing placement, asset loading, or content rendered outside the element subtree |
Treat the generated DOM beneath a Quasar component as private implementation detail. Work through the highest owning layer that expresses the requirement.
## API Mapping Across Layers
| Requirement | NiceGUI surface | Underlying mechanic |
| --- | --- | --- |
| Wrapper-supported value or behavior | constructor argument, binding, or helper such as `set_options()` | Python normalizes state and synchronizes the component |
| Additional Quasar option | `.props(...)` | values become props on the wrapped Vue component |
| Browser or Quasar notification | constructor callback or `.on(...)` | an emitted frontend event is forwarded to a Python handler |
| Semantic insertion point | `add_slot(...)` or a wrapper-specific slot API | content renders in a named Vue slot |
| Imperative frontend action | a NiceGUI helper or `run_method(...)` | NiceGUI invokes a public method on the client component |
| Page placement or appearance | `.classes(...)`, `.style(...)`, or an application stylesheet | CSS applies to the rendered element; detached content needs its own class hook |
Constructor data remains in Python, Quasar props cross through `.props()`, emitted events cross through callbacks or `.on()`, and named Vue slots cross through NiceGUI's slot API. A Vue example in the Quasar documentation therefore maps to several distinct NiceGUI surfaces rather than to one copied template.
## State And Event Flow
Server-driven changes and user-driven changes cross a client-server boundary:
1. Python creates the wrapper and serializes initial state to the client.
2. Vue renders the Quasar component from those props and slots.
3. A browser interaction causes Quasar to update client state or emit an event.
4. NiceGUI forwards registered events to Python handlers.
5. Python mutations return through bindings, wrapper helpers, or an explicit `update()`.
Wrapper helpers and bindings preserve NiceGUI's value model and schedule the corresponding client update. Directly changing a plain Python collection or constructing a raw JavaScript object does not itself imply that the client receives the change.
## Detached Content And Assets
Some Quasar components render menus, dialogs, tooltips, and similar content outside the field or trigger's DOM subtree. A descendant CSS selector beneath the Python-created element will not reach that content. Component APIs expose props such as `popup-content-class` for assigning a separate class hook to detached content.
Icons and other externally defined visuals add another boundary: a valid Quasar icon name identifies an asset but does not load its font or stylesheet. Confirm both the naming convention and the application-level asset registration.
## Versioned Sources
The exact public surface depends on both the installed NiceGUI version and the Quasar version bundled with it. NiceGUI's tagged `package.json` records that pairing. The component details below describe NiceGUI `3.16.0` with Quasar `2.18.5`, as declared by [NiceGUI `v3.16.0` frontend dependencies](https://github.com/zauberzeug/nicegui/blob/v3.16.0/package.json).
Four source levels answer different questions:
| Source | Information it defines |
| --- | --- |
| NiceGUI component documentation | documented Python constructors, callbacks, methods, and examples |
| NiceGUI wrapper source at the installed tag | normalization, validation, stored properties, bindings, updates, and the wrapped frontend component |
| Quasar component API at the bundled tag | accepted props, emitted events, named slots, public methods, accessibility behavior, and warnings |
| Quasar component source at the bundled tag | detailed runtime behavior behind that public API |
Links to `main`, `dev`, or the latest hosted documentation can describe a newer API than the installed package. Tagged NiceGUI and matching `quasar-v<version>` links provide the version-specific definition.
## Using Slots In NiceGUI
A NiceGUI element is the Python-side representation of a browser component. Many elements wrap Quasar Vue components, whose insertion points are exposed as slots. A simple container normally uses one default slot; more complex components expose named slots such as `prepend`, `append`, `option`, `header`, or `body-cell-*`. The available names and their contracts belong to the wrapped component, so verify them in the version-matched Quasar documentation.
NiceGUI creates a default slot for every element. Entering an element as a context manager enters that default slot, and entering `element.add_slot(name)` selects a named slot. NiceGUI keeps the active slots on a task-local stack; each element constructed inside the `with` block becomes a child of the innermost active slot.
These mechanics are defined by the tagged [`Element.add_slot()` implementation](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/element.py), the [`Slot` context manager](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/slot.py), and NiceGUI's [context-managed scoped-slot examples](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/table_documentation.py).
### Prefer Python-Owned Composition
Use NiceGUI context managers and `ui.*` elements for slot structure whenever they can represent the required element tree. Keep values, mappings, validation, permissions, event handling, and authoritative state transitions in Python. This preserves element identity, typed wrapper APIs, lifecycle cleanup, test visibility, and the normal NiceGUI update path.
Use the narrowest browser-side expression for state that exists only while Quasar renders a scoped slot. A dynamic prop such as `:label="props.value"` may project that value into a NiceGUI element without moving the surrounding structure or business rules into JavaScript. When Python needs a browser-owned value, emit the smallest serializable proposal to a Python handler and validate it there.
Escalate to `add_slot(name, template)` only when the slot contract requires client-side structure that context-managed NiceGUI elements cannot preserve, such as a browser-side `v-for`, a variable number of sibling roots, or Vue's object form of `v-bind` for a Quasar interaction bundle. Keep raw templates small, use documented scoped props, and do not duplicate authoritative application logic in JavaScript.
### Context-Managed NiceGUI Elements
Ordinary NiceGUI elements can populate slot content:
```python
name_input = ui.input("Name")
with name_input.add_slot("prepend"):
ui.icon("person")
```
Nested context managers express the component hierarchy while preserving NiceGUI element identity, event registration, updates, deletion, and test visibility.
### Scoped Props On The Client
A scoped slot is a function whose argument is supplied by the component that renders the slot. Vue calls that argument the slot props; `props` is only NiceGUI's chosen local name for it. Since NiceGUI `3.5.0`, context-managed NiceGUI elements inside a scoped slot receive the current slot-props object as their frontend render context.
The general `.props()` grammar and dynamic binding path are described under [Props](#props). In this context, the current scope object can be referenced by dynamic properties and JavaScript event handlers. For example:
```python
ui.badge().props(
':label=props.label :color="props.selected ? \'primary\' : \'grey\'"'
)
```
corresponds conceptually to this Vue template:
```vue
<q-badge :label="props.label" :color="props.selected ? 'primary' : 'grey'" />
```
Static `.props()` values do not have access to the slot scope. Only colon-prefixed expressions and NiceGUI JavaScript event handlers are evaluated with `props` in scope.
#### Which `props.*` Names Exist
There is no global catalog of `props.*` attributes. The owner of each named slot chooses the keys it passes when invoking that slot, so the available names can differ between components and between slots on the same component. Find them in this order:
1. Open the wrapped component's version-matched Quasar API and inspect the **Slots** entry for the exact named slot.
2. Use the slot's `scope` table as the public contract, including each value's type and whether it is data, state, or a callable.
3. Inspect the version-matched Quasar source only when the API does not explain a bundle's contents or runtime behavior.
For example, the [`QSelect` `option` slot API at Quasar `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/select/QSelect.json) exposes:
| Expression | Meaning |
| --- | --- |
| `props.index` | index in the options array |
| `props.opt` | original option from the `options` prop |
| `props.label` | label after `option-label` processing |
| `props.html` | whether the option content is marked as HTML |
| `props.selected` | whether this option is selected |
| `props.focused` | whether this option is the focused menu option |
| `props.toggleOption` | function that adds or removes an option from the model |
| `props.setOptionIndex` | function that changes the focused option index |
| `props.itemProps` | object of computed props and listeners intended for the root `QItem` |
The tagged [`QSelect` implementation](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/select/QSelect.js) constructs `itemProps` with values such as `clickable`, `active`, `activeClass`, `manualFocus`, `focused`, `disable`, `tabindex`, `dense`, `dark`, `role`, `aria-selected`, `id`, `onClick`, and, when applicable, `onMousemove`. It is a behavior and accessibility bundle, not the original option object. Other QSelect slots expose different scopes: `no-option` only documents `inputValue`, while `selected-item` documents selection-oriented keys such as `index`, `opt`, `removeAtIndex`, `toggleOption`, and `tabindex`. A QTable body-cell slot's `props.value` is valid because QTable supplies `value`; that name should not be assumed in a QSelect option slot.
#### Sending Scoped Values To Python
NiceGUI also places the current slot object in scope while evaluating a `js_handler`. Use the event bridge's `emit(...)` function to select or transform JSON-serializable values before the Python callback runs:
```python
ui.button("Inspect").on(
"click",
handler=lambda event: print(event.args),
js_handler="() => emit({index: props.index, label: props.label})",
)
```
Scoped props exist only in the browser render context. They are not Python variables and cannot be read by a Python callback until a JavaScript handler emits the required values. Treat `innerHTML`, `v-html`, and raw template interpolation as untrusted HTML unless the source is explicitly sanitized.
### Slot Contracts
Replacing default slot content also replaces the wrapped component's default rendering. Documented slot-prop bundles can carry behavior as well as data. For example, a `QSelect` option slot binds `props.itemProps` to its root item; without that binding, the custom row can lose click selection, disabled state, focus, active state, and keyboard navigation. Quasar's virtual-scroll contract expects one root element per item unless additional siblings carry its documented marker class.
## `ui.select`
### Versioned Source Definitions
- **NiceGUI documentation:** [`ui.select` documentation source at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/select_documentation.py)
- **NiceGUI source code:** [`Select` implementation at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/select.py)
- **Quasar documentation:** [`QSelect` documentation source at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/docs/src/pages/vue-components/select.md)
- **Quasar source code:** [`QSelect` implementation at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/select/QSelect.js)
### Layer Ownership
NiceGUI's `Select` wraps Quasar `QSelect` but owns important Python-side behavior. Its constructor handles options, labels, values, change callbacks, input filtering, new-value modes, multiple selection, clearing, validation, and key generation. Use those constructor parameters before adding equivalent Quasar props manually.
### Exposed Surfaces
- The NiceGUI constructor exposes `options`, `label`, `value`, `on_change`, `with_input`, `new_value_mode`, `multiple`, `clearable`, `validation`, and `key_generator`.
- `.props()` carries additional documented `QSelect` behavior such as field design, chips, option density, popup classes, popup positioning, and menu/dialog behavior.
- `.classes()` attaches structural width, placement, and other CSS utilities to the field element.
- Named slots provide prepend, append, loading, no-option, selected, and option content.
- Scoped-slot props retain Quasar's selection and keyboard behavior when option content is replaced.
### Example: Custom Menu Options With A Scoped Slot
`QSelect` supplies each option as `props.opt`, its processed label as `props.label`, and its interaction contract as `props.itemProps`. Because the complete interaction bundle needs Vue's object form of `v-bind`, use a raw slot template for the root item:
```python
from nicegui import ui
item_select = ui.select(
options={"chair": "Chair", "desk": "Desk", "lamp": "Lamp"},
label="Item",
value="chair",
clearable=True,
with_input=True,
).props("outlined options-dense")
with item_select.add_slot("prepend"):
ui.icon("search")
item_select.add_slot(
"option",
r"""
<q-item v-bind="props.itemProps">
<q-item-section avatar>
<q-icon name="inventory_2" />
</q-item-section>
<q-item-section>
<q-badge :label="props.label" outline color="primary" />
</q-item-section>
</q-item>
""",
)
```
The `prepend` slot uses context-managed NiceGUI elements because it needs no scoped object spread. The raw `option` template is compiled by Vue, so `v-bind="props.itemProps"` forwards every computed property and listener to `QItem`; the badge reads the processed browser-side label. Keep that binding on the root item so the custom rendering retains the option's interaction and accessibility wiring.
### Behavioral Caveats
These caveats are distilled from the four version-matched sources above:
- NiceGUI accepts a list of values or a dictionary mapping values to labels. Do not assume the Python options model is the same as Quasar's JavaScript object-array examples.
- After mutating `options`, call `update()` or use `set_options()` so the client receives the change.
- `new_value_mode` enables input automatically. For dictionary options with `add`, NiceGUI requires a `key_generator`.
- A multiple select has a list value. NiceGUI normalizes a non-list initial value, but application state should still use the intended list shape.
- `map-options` has a Quasar performance cost. Do not add it to NiceGUI's mapped options without confirming that the wrapper's value translation requires it.
- `display-value-html` and `options-html` can create cross-site scripting risk. When using `selected`, `selected-item`, or `option` slots, the application owns sanitization.
- A custom `option` slot must bind `props.itemProps` to its root `QItem` so click, focus, active, disabled, and keyboard behavior remain connected.
- Custom option slots use virtual scrolling. When one option renders multiple sibling elements, Quasar requires `q-virtual-scroll--with-prev` on every additional sibling.
- Buttons placed in `before`, `after`, `prepend`, or `append` field slots do not propagate clicks to the parent. A submit button in one of those slots needs its own submit handler.
- `QSelect` renders its popup outside the field. Style it through `popup-content-class`; do not assume a descendant selector beneath the field will reach it.
- Quasar switches between menu and dialog popup behavior by platform. Verify forced `behavior=menu` carefully on iOS when input filtering is enabled.
`.on()` and `run_method()` address events and methods defined by the installed Quasar API. NiceGUI's `on_change`, `set_options()`, value bindings, and `is_showing_popup` provide wrapper-managed equivalents for their respective behaviors.
## `ui.icon`
### Versioned Source Definitions
- **NiceGUI documentation:** [`ui.icon` documentation source at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/website/documentation/content/icon_documentation.py)
- **NiceGUI source code:** [`Icon` implementation at `v3.16.0`](https://github.com/zauberzeug/nicegui/blob/v3.16.0/nicegui/elements/icon.py)
- **Quasar documentation:** [`QIcon` documentation source at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/docs/src/pages/vue-components/icon.md)
- **Quasar source code:** [`QIcon` implementation at `2.18.5`](https://github.com/quasarframework/quasar/blob/quasar-v2.18.5/ui/src/components/icon/QIcon.js)
### Layer Ownership
NiceGUI's `Icon` is a thin `QIcon` wrapper. Its constructor exposes `name`, `size`, and `color`; the source forwards these to a `q-icon` element. Use Quasar's icon naming and asset rules for anything beyond those parameters.
### Exposed Surfaces
- The application-loaded icon family determines which icon names can render.
- `ui.icon()` accepts the documented icon name, size, and color.
- `.props()` carries supported `QIcon` props such as `left`, `right`, and a custom render tag.
- `.classes()` controls structural placement and can attach application-defined visual variants.
- Static stylesheets define Material Symbol axes, state variants, custom webfonts, and repeated effects.
### Example
```python
from nicegui import ui
ui.icon(
"sym_o_home",
size="1.5rem",
color="primary",
).classes(
"app-symbol-filled shrink-0"
).tooltip(
"Home"
)
```
```css
.app-symbol-filled {
font-variation-settings:
"FILL" 1,
"wght" 400,
"GRAD" 0,
"opsz" 24;
}
```
### Behavioral Caveats
These caveats are distilled from the four version-matched sources above:
- Material icon names use snake case. Material variants use prefixes such as `o_`, `r_`, `s_`, `sym_o_`, `sym_r_`, and `sym_s_`.
- Other icon families have their own prefixes and require their webfont or stylesheet to be loaded. A valid name does not load the corresponding asset.
- `size` accepts CSS units or Quasar sizes such as `xs`, `sm`, `md`, `lg`, and `xl`. Quasar implements icon sizing through `font-size`.
- Icon color inherits text color unless the `color` prop or a CSS color overrides it.
- Material Symbol variable axes apply to webfont icons, not static SVG icon exports.
- Quasar also supports SVG path strings, `svguse:` references, and `img:` URLs. Confirm the exact `QIcon` name format and mount path before generating one of these forms.
- `QIcon` renders with `aria-hidden="true"`. For an action, use a semantic control such as `ui.button(icon=..., on_click=...)` and put the accessible name on that control; a tooltip is supplementary.
- Prefer `ui.icon(...).tooltip(...)` over manually constructing tooltip slot markup when NiceGUI's method covers the visual hint.
## Related Reference Index
- [NiceGUI component documentation](https://nicegui.io/documentation): Python constructors, callbacks, bindings, and wrapper methods
- [NiceGUI `Element` documentation](https://nicegui.io/documentation/element): common props, classes, styles, hierarchy, updates, and client methods
- [NiceGUI generic events](https://nicegui.io/documentation/generic_events): `.on()`, event arguments, JavaScript handlers, and throttling
- [NiceGUI binding documentation](https://nicegui.io/documentation/section_binding_properties): one-way and two-way Python property binding
- [Quasar component documentation](https://quasar.dev/vue-components): per-component props, events, slots, and methods
- [Quasar breakpoints](https://quasar.dev/style/breakpoints): viewport names and pixel thresholds
- [Quasar spacing classes](https://quasar.dev/style/spacing): padding and margin class syntax and permutations
- [Quasar visibility classes](https://quasar.dev/style/visibility): responsive, platform, orientation, and print visibility
- [Quasar helper classes](https://quasar.dev/style/other-helper-classes): pointer, scrolling, sizing, rotation, and border helpers
- [Tailwind CSS documentation](https://tailwindcss.com/docs): complete utility-class categories and variant syntax
- [MDN CSS reference](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference): CSS properties accepted by `.style()` and application stylesheets
- [MDN event reference](https://developer.mozilla.org/en-US/docs/Web/Events): native browser event names and behavior
@@ -0,0 +1,194 @@
# NiceGUI Configuration And Deployment
Use this reference when a NiceGUI task concerns `ui.run(...)` settings, runtime URLs, native windows, environment variables, server hosting, executable packaging, or NiceGUI On Air. For startup ownership, app factories, `ui.run_with(...)`, lifespan, reload, and workers, load [FastAPI and Uvicorn startup](./fastapi-uvicorn-startup.md).
The public surfaces below follow NiceGUI's current [configuration and deployment documentation](https://nicegui.io/documentation/section_configuration_deployment). Inspect the target project's pinned NiceGUI version before relying on a recently added option or native-mode behavior.
## Configure The Owning Runtime
Choose the process owner before setting runtime options:
| Deployment shape | Owning surface | Where configuration belongs |
| --- | --- | --- |
| NiceGUI is the application and starts its server | `ui.run(...)` | NiceGUI arguments plus additional Uvicorn keyword arguments |
| A parent FastAPI app owns startup | `ui.run_with(parent_app, ...)` and the external ASGI server | NiceGUI composition options in `ui.run_with`; socket, TLS, reload, and worker options in Uvicorn or the process manager |
| Desktop application | `ui.run(native=True, ...)` | NiceGUI runtime options and `app.native` configuration |
| Packaged browser or desktop executable | `ui.run(reload=False, ...)` | import-safe page registration, packaging flags, and multiprocessing setup |
Do not split ownership by calling `ui.run()` and a separate server launcher for the same app. The exact composition patterns and worker constraints are in [FastAPI and Uvicorn startup](./fastapi-uvicorn-startup.md).
## Select `ui.run` Options Deliberately
[`ui.run(...)`](https://nicegui.io/documentation/run) accepts several groups of settings:
| Concern | Representative options | Decision rule |
| --- | --- | --- |
| route and metadata | `root`, `title`, `viewport`, `favicon`, `language`, `dark`, `markdown` | Use a root callable or decorated pages; override metadata per page when it is route-specific. |
| network and launch | `host`, `port`, `show`, `on_air` | Bind and expose only the interfaces required by the deployment; treat On Air as a separate remote-access choice. |
| client recovery | `reconnect_timeout`, `message_history_length` | Tune together from observed disconnect duration and replay volume; replay is not durable job delivery. |
| binding work | `binding_refresh_interval` | Reduce active links before lowering the interval; use `None` only when no polling-based links need updates. |
| static delivery | `cache_control_directives`, `gzip_middleware_factory` | Preserve deliberate cache lifetimes and compression behavior; disabling gzip or changing immutable caching is an operational decision. |
| development | `reload`, `uvicorn_reload_dirs`, `uvicorn_reload_includes`, `uvicorn_reload_excludes`, `uvicorn_logging_level` | Keep reload local to development and restart fully when changing options that the reloader process owns. |
| frontend runtime | `tailwind`, `unocss`, `prod_js` | Verify class compatibility before switching CSS engines; use production Vue and Quasar assets in deployed apps. |
| API visibility | `fastapi_docs`, `endpoint_documentation` | Expose only the OpenAPI surfaces the application intends to publish. |
| browser storage | `storage_secret`, `session_middleware_kwargs` | A secret is required for `ui.storage.user` and `ui.storage.browser`; load it from a secret source and configure cookie policy for the deployment. |
| native window | `native`, `window_size`, `fullscreen`, `frameless` | Use only for a desktop app with a supported browser engine. |
Additional keyword arguments are forwarded to `uvicorn.run`. Most `ui.run` option changes require stopping and fully restarting the process; do not assume development auto-reload applies them.
## Read Runtime URLs After Binding
[`app.urls`](https://nicegui.io/documentation/section_configuration_deployment#urls) contains the URLs on which the running app is available. The server has not bound its sockets during `app.on_startup`, so the collection is not available there. Read it in a page function or subscribe to `app.urls.on_change` when another application component needs the final addresses.
```python
from nicegui import app, ui
@ui.page('/')
def home() -> None:
for url in app.urls:
ui.link(url, target=url)
ui.run()
```
Do not derive a public URL solely from the listening host and port when a reverse proxy, container port mapping, or tunnel owns the external address.
## Configure Environment-Controlled Facilities
NiceGUI recognizes these framework environment variables:
| Variable | Default | Effect |
| --- | --- | --- |
| `MATPLOTLIB` | enabled | Set to `false` to skip the potentially costly Matplotlib import; `ui.pyplot` and `ui.line_plot` then remain unavailable. |
| `NICEGUI_STORAGE_PATH` | `.nicegui` in the working directory | Changes the local storage-file directory. |
| `NICEGUI_REDIS_URL` | no Redis backend | Selects Redis for shared persistent storage. |
| `NICEGUI_REDIS_KEY_PREFIX` | `nicegui:` | Namespaces NiceGUI keys in Redis. |
| `MARKDOWN_CONTENT_CACHE_SIZE` | `1000` | Bounds cached Markdown snippets. |
| `RST_CONTENT_CACHE_SIZE` | `1000` | Bounds cached reStructuredText snippets. |
Treat these as process-start configuration. For application-owned host, port, credentials, feature flags, and service settings, use one validated settings model rather than scattering direct environment reads. When multiple processes or executables share local storage, do not let them independently rewrite the same files; give each instance a distinct `NICEGUI_STORAGE_PATH` or configure Redis where state must be shared.
## Deploy A Browser-Hosted App
Run the production entry point under a service manager or container restart policy. NiceGUI's [multi-architecture Docker image](https://hub.docker.com/r/zauberzeug/nicegui) runs an application mounted at `/app`; its default internal port is `8080`, so publish that port explicitly. The image supports non-root execution through `PUID` and `PGID` and passes process signals through to the app.
```bash
docker run --detach --restart always \
--publish 80:8080 \
--env PUID="$(id -u)" \
--env PGID="$(id -g)" \
--volume "$PWD:/app" \
zauberzeug/nicegui:latest
```
For HTTPS, either pass Uvicorn's `ssl_certfile` and `ssl_keyfile` options to `ui.run(...)` or terminate TLS at a reverse proxy such as NGINX or Traefik. A reverse-proxy deployment must preserve NiceGUI's HTTP and Socket.IO traffic, forwarding scheme and host information, route prefixes, timeouts, and upload limits consistently. Verify the rendered page, static assets, websocket connection, reconnect behavior, and upload path through the public URL rather than only against the container port.
Use one worker by default. NiceGUI clients, element trees, tasks, and ordinary Python state are process-local; a multi-worker deployment needs compatible session affinity and externalized shared state. See [FastAPI and Uvicorn startup](./fastapi-uvicorn-startup.md#development-reload) before adding workers or combining them with reload.
## Build A Native Desktop App
[`ui.run(native=True)`](https://nicegui.io/documentation/section_configuration_deployment#native-mode) launches a pywebview window. `window_size`, `fullscreen`, and `frameless` cover common presentation settings. Configure lower-level pywebview behavior before startup through:
- `app.native.window_args` for `webview.create_window` arguments
- `app.native.start_args` for `webview.start` arguments
- `app.native.settings` for pywebview settings
- `app.native.main_window` for asynchronous access to the running window
Values in `window_args` and `start_args` take precedence over overlapping `ui.run` arguments. The browser engine must support ES modules and import maps; use Chrome 89 or newer, a current WebKitGTK or Qt backend on Linux, and the EdgeChromium prerequisites used by pywebview on Windows. A local Windows `favicon` used as the native icon must be an `.ico` file.
Native mode chooses an available port automatically when `port` is omitted. Browser mode defaults to `8080`; use `native.find_open_port()` explicitly when multiple browser-mode executable instances must coexist.
### Native Events And Process Placement
Register sync or async handlers with `app.native.on(...)`. Supported lifecycle and window events are `shown`, `loaded`, `minimized`, `maximized`, `restored`, `resized`, `moved`, `closed`, and `drop`. Resized and moved events expose dimensions or coordinates in `event.args`; drop events expose filesystem paths under `event.args['files']`.
The native UI runs in a separate process. Define `app.native.window_args`, `start_args`, `settings`, and event registrations outside the `if __name__ == '__main__':` guard so the child process sees them.
```python
from nicegui import app, ui
app.native.window_args['resizable'] = False
app.native.on('drop', lambda event: print(event.args['files']))
if __name__ == '__main__':
ui.run(native=True, reload=False)
```
Native storage follows the same scopes as browser mode. Multiple executable instances started from one working directory can collide on the default `.nicegui` files; isolate `NICEGUI_STORAGE_PATH` per instance or use Redis for intentionally shared state.
## Package An Executable
Both `nicegui-pack`/PyInstaller and Nuitka require an import-safe application:
1. Disable auto-reload with `ui.run(reload=False, ...)`.
2. Supply a `root` page callable to `ui.run` or register at least one `@ui.page`.
3. Decide whether the executable opens a browser or uses `native=True`.
4. Use an available port when simultaneous instances are valid.
5. Exercise the built artifact on every target operating system; a successful build on the development host does not establish runtime compatibility.
With [`nicegui-pack`](https://nicegui.io/documentation/section_configuration_deployment#package-for-installation), `--onefile` is convenient but starts more slowly because PyInstaller extracts it on each run. A directory build starts faster and can be archived for distribution. Use `--windowed` only with `native=True`; a browser-mode application without a console has no normal Ctrl-C exit surface.
Nuitka must include both NiceGUI modules and package data because NiceGUI uses lazy imports and ships frontend assets:
```bash
python -m nuitka \
--onefile \
--include-package=nicegui \
--include-package-data=nicegui \
main.py
```
Add equivalent package and package-data flags for optional libraries that ship templates or frontend assets. Prefer `--standalone` when startup speed matters more than producing one file.
### Multiprocessing In Packaged Native Apps
Packaged native apps must call [`multiprocessing.freeze_support()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support) as the first statement inside the main guard to prevent recursive process creation. Keep native settings outside the guard so the spawned native process applies them.
```python
from multiprocessing import freeze_support
from nicegui import app, ui
app.native.window_args['transparent'] = True
def root() -> None:
ui.label('Packaged app')
if __name__ == '__main__':
freeze_support()
ui.run(root, native=True, reload=False)
```
## Use On Air Only For Deliberate Remote Access
[`ui.run(on_air=True)`](https://nicegui.io/documentation/section_configuration_deployment#nicegui-on-air) creates a temporary public URL, currently valid for one hour. A private device token can select a stable organization/device URL. Treat that token as a secret, and do not log or commit it.
NiceGUI On Air is a tech preview, not a substitute for selecting an authentication, authorization, availability, and data-governance model. Before exposing an application, review what data and actions become reachable, add application authentication where needed, and verify the service's current operational and privacy terms. Use ordinary hosted deployment when the application requires controlled networking, durable availability, or organization-owned TLS and access policy.
## Deployment Verification
Validate the built deployment through its real entry point and public boundary:
- process starts with reload disabled and shuts down cleanly under the service manager or container runtime
- health route, root page, static assets, Socket.IO connection, and reconnect flow work through the proxy or published port
- `app.urls` is consumed only after server binding and is not mistaken for canonical proxy configuration
- storage survives and isolates users, tabs, workers, and executable instances as designed
- TLS, forwarded headers, cookie flags, upload limits, cache policy, and logs match the public deployment
- a native build opens, handles window events, closes cleanly, and can run alongside another instance when supported
- packaged artifacts include NiceGUI and optional-library data files and are tested on each target platform
- normal background tasks cancel on shutdown, while only explicitly bounded finalization work uses `@background_tasks.await_on_shutdown`; see [interaction mechanics](./interaction-patterns.md#execution-contexts)
## Sources
!!! info "Primary sources"
- [NiceGUI configuration and deployment](https://nicegui.io/documentation/section_configuration_deployment)
- [`ui.run` arguments](https://nicegui.io/documentation/run)
- [NiceGUI Docker example](https://github.com/zauberzeug/nicegui/tree/main/examples/docker_image)
- [NiceGUI NGINX HTTPS example](https://github.com/zauberzeug/nicegui/blob/main/examples/nginx_https/nginx.conf)
- [NiceGUI FastAPI example](https://github.com/zauberzeug/nicegui/tree/main/examples/fastapi)
- [pywebview API](https://pywebview.flowrl.com/api)
- [Uvicorn settings](https://www.uvicorn.org/settings/)

Some files were not shown because too many files have changed in this diff Show More