prompt markdown
This commit is contained in:
+6
-10
@@ -11,11 +11,11 @@ The application combines a FastMCP server with a pre-built Zensical documentatio
|
||||
The runtime has four content paths:
|
||||
|
||||
1. `SkillsDirectoryProvider` publishes native `skill://` resources from packaged skill directories.
|
||||
2. `FileSystemProvider` discovers typed `@prompt` functions from packaged Python modules.
|
||||
2. A custom prompt provider loads declarative prompt definitions from packaged Markdown.
|
||||
3. The general docs registry publishes non-skill Markdown through `resource://docs/{path*}`.
|
||||
4. FastAPI serves the pre-built `site/` directory.
|
||||
|
||||
There is no custom skill catalog, prompt catalog, or prompt registry model.
|
||||
There is no custom skill catalog, prompt catalog, or per-prompt Python module.
|
||||
|
||||
## Source Ownership
|
||||
|
||||
@@ -36,12 +36,9 @@ The provider parses standard skill frontmatter and generates the manifest. The g
|
||||
|
||||
### Prompts
|
||||
|
||||
Each prompt has two coordinated sources:
|
||||
Each prompt has one source: `docs/prompts/<prompt-id>/PROMPT.md`. Its nested `prompt` frontmatter owns runtime metadata and argument declarations, while its body owns canonical prose.
|
||||
|
||||
1. `src/personal_mcp/prompts/components/<module>.py` owns the typed signature and runtime metadata.
|
||||
2. `docs/prompts/<prompt-id>/PROMPT.md` owns the canonical prompt prose.
|
||||
|
||||
The component loads Markdown with `importlib.resources`. The renderer strips documentation frontmatter, requires exact placeholder-to-argument equality, and substitutes typed values. `FileSystemProvider(reload=False)` discovers the components during server construction.
|
||||
The custom provider reads packaged Markdown with `importlib.resources`, validates metadata and exact placeholder-to-argument equality, and creates native FastMCP prompt objects. It rescans on each list and get request, so an editable deployment observes file additions, edits, and deletions without a restart.
|
||||
|
||||
FastMCP exposes prompts through native `prompts/list` and `prompts/get` operations.
|
||||
|
||||
@@ -54,8 +51,7 @@ The docs registry indexes packaged Markdown for `resource://docs/{path*}`. It re
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Packaged Skill Directories] --> B[SkillsDirectoryProvider]
|
||||
C[Typed Prompt Components] --> D[FileSystemProvider]
|
||||
E[Packaged Prompt Markdown] --> C
|
||||
C[Packaged Prompt Markdown] --> D[Markdown Prompt Provider]
|
||||
F[General Markdown] --> G[Docs Registry]
|
||||
B --> H[FastMCP Server]
|
||||
D --> H
|
||||
@@ -65,7 +61,7 @@ flowchart TD
|
||||
K --> M
|
||||
```
|
||||
|
||||
Server construction is lazy with respect to package import. Each application process creates its providers and docs snapshot when the server factory runs. Production providers use `reload=False`; content changes require a process restart.
|
||||
Server construction is lazy with respect to package import. Each application process creates its providers and docs snapshot when the server factory runs. Skills use startup discovery, while prompts are reloaded when a client lists or gets prompts.
|
||||
|
||||
## Packaging
|
||||
|
||||
|
||||
+8
-8
@@ -72,16 +72,16 @@ Recommended sequence:
|
||||
|
||||
## Prompt Authoring
|
||||
|
||||
Prompts pair typed Python metadata with canonical Markdown prose:
|
||||
A prompt is one self-describing `docs/prompts/<prompt-id>/PROMPT.md` file:
|
||||
|
||||
1. Add one `@prompt` function under `src/personal_mcp/prompts/components/`.
|
||||
2. Use the function signature for arguments, requiredness, and literal constraints.
|
||||
3. Set name, description, tags, and version on the decorator.
|
||||
4. Keep the canonical body in `docs/prompts/<prompt-id>/PROMPT.md`.
|
||||
5. Keep Markdown placeholders exactly equal to the Python argument names.
|
||||
6. Use only documentation-site fields in Markdown frontmatter.
|
||||
1. Create a lowercase kebab-case directory beneath `docs/prompts/`.
|
||||
2. Add a nested `prompt` frontmatter mapping with version, description, tags, and ordered arguments.
|
||||
3. Give every argument a description and explicit required flag.
|
||||
4. Add `choices` only when a string argument accepts a fixed set of values.
|
||||
5. Use each argument exactly once or more as a `{{argument_name}}` placeholder in the body.
|
||||
6. Do not add a Python component, name field, metadata sidecar, or central catalog entry.
|
||||
|
||||
The production `FileSystemProvider` discovers component modules. Do not add prompt registry models, catalog resources, or dynamic signature generation.
|
||||
The custom provider rescans prompt documents during every native list and get request. Changes in an editable checkout are therefore visible on the next request without a process restart. Invalid metadata or placeholder drift fails that request with a configuration error.
|
||||
|
||||
## Frontmatter Safety
|
||||
|
||||
|
||||
@@ -29,20 +29,28 @@ The provider uses the directory name as the URI identity and the frontmatter `de
|
||||
|
||||
## Prompt Documentation Frontmatter
|
||||
|
||||
Prompt runtime metadata is defined by typed Python components, not Markdown frontmatter. A prompt document may retain only fields consumed by the static documentation site:
|
||||
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
|
||||
---
|
||||
icon: lucide/messages-square
|
||||
prompt:
|
||||
version: "1.0.0"
|
||||
description: Describe when to use the prompt.
|
||||
tags: [example, prompts]
|
||||
arguments: {topic: {description: "Topic to process.", required: true, choices: [first, second]}, notes: {description: "Optional constraints.", required: false}}
|
||||
---
|
||||
```
|
||||
|
||||
Prompt rules:
|
||||
|
||||
1. Do not duplicate prompt names, descriptions, versions, tags, or arguments in Markdown YAML.
|
||||
2. Argument names must be valid Python identifiers in the component signature.
|
||||
3. Literal value constraints belong in Python type annotations.
|
||||
4. Markdown placeholders must exactly match the component argument names.
|
||||
1. `version`, `description`, `tags`, and `arguments` are required; unknown fields inside `prompt` or an argument are rejected.
|
||||
2. The directory name supplies the prompt id. Do not add a duplicate `name` field.
|
||||
3. Argument names must be valid identifiers and preserve their authored mapping order.
|
||||
4. Every argument requires a non-empty `description` and explicit `required` boolean.
|
||||
5. Optional `choices` must be a non-empty list of unique, non-empty strings.
|
||||
6. Markdown placeholders must exactly match the declared argument names.
|
||||
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.
|
||||
|
||||
@@ -54,11 +62,11 @@ Skill validation is file- and provider-oriented:
|
||||
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.
|
||||
|
||||
Prompt validation is provider- and renderer-oriented. Provider discovery validates decorated functions, while focused tests render every prompt and reject placeholder drift.
|
||||
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
|
||||
|
||||
1. Skills remain directly portable to tools that understand standard Agent Skills directories.
|
||||
2. Native skill discovery has no parallel catalog metadata source.
|
||||
3. Prompts use FastMCP's native component metadata and protocol surface without a parallel catalog.
|
||||
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/`.
|
||||
|
||||
+25
-23
@@ -4,11 +4,11 @@ icon: lucide/messages-square
|
||||
|
||||
# Prompt Contract
|
||||
|
||||
This page defines the canonical contract for typed prompts discovered by FastMCP's `FileSystemProvider`.
|
||||
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 has a Python component and one canonical Markdown document:
|
||||
Each prompt is one self-describing Markdown document:
|
||||
|
||||
```mermaid
|
||||
---
|
||||
@@ -22,31 +22,32 @@ config:
|
||||
lineColor: '#FFFFFF'
|
||||
---
|
||||
treeView-beta
|
||||
"src/personal_mcp/prompts/"
|
||||
"components/"
|
||||
"<prompt_module>.py"
|
||||
"content.py"
|
||||
"provider.py"
|
||||
"docs/prompts/"
|
||||
"<prompt-id>/"
|
||||
"PROMPT.md"
|
||||
"src/personal_mcp/prompts/"
|
||||
"content.py"
|
||||
"models.py"
|
||||
"provider.py"
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
1. Each component exports one typed function decorated with `@prompt`.
|
||||
2. Function parameters define the MCP argument names, requiredness, and accepted values.
|
||||
3. Decorator fields define runtime name, description, tags, and version.
|
||||
4. The function loads its matching `docs/prompts/<prompt-id>/PROMPT.md` through `importlib.resources`.
|
||||
5. `PROMPT.md` owns the rendered prompt prose and uses `{argument_name}` placeholders.
|
||||
6. The renderer requires exact equality between the function's arguments and the Markdown placeholders.
|
||||
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. Python owns runtime metadata and the callable schema.
|
||||
2. Markdown owns prompt prose and may contain only documentation-site frontmatter.
|
||||
3. There is no custom prompt catalog, prompt registry model, or metadata sidecar.
|
||||
4. `FileSystemProvider(reload=False)` discovers components when the server is created.
|
||||
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 `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
|
||||
|
||||
@@ -57,7 +58,7 @@ Rules:
|
||||
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 `@prompt` name and Markdown directory name must equal `prompt-id`.
|
||||
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:
|
||||
@@ -74,9 +75,10 @@ Invalid examples:
|
||||
|
||||
## Rendering Contract
|
||||
|
||||
1. The renderer strips one leading YAML frontmatter block before returning prompt content.
|
||||
2. Required values are supplied by the typed function signature.
|
||||
3. An omitted optional value renders as `Not provided`.
|
||||
4. Unknown prompt ids and mismatched placeholders fail immediately.
|
||||
5. Prompt content is read from packaged resources and does not depend on the working directory.
|
||||
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.
|
||||
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ Instructions steer behavior but do not force VS Code to attach resources automat
|
||||
|
||||
## 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.
|
||||
Prompts remain separate from skills. When the client supports MCP prompt APIs, use prompt listing and `get_prompt` for parameterized workflows. Each authored `PROMPT.md` is the complete source of truth for its metadata, arguments, and prose; changes are loaded on the next prompt request.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
+6
-6
@@ -28,8 +28,8 @@ treeView-beta
|
||||
"static build output"
|
||||
"src/personal_mcp"
|
||||
"mcp.py"
|
||||
"prompts/components/*.py"
|
||||
"prompts/content.py"
|
||||
"prompts/models.py"
|
||||
"prompts/provider.py"
|
||||
"registry/"
|
||||
"skills/provider.py"
|
||||
@@ -39,7 +39,7 @@ treeView-beta
|
||||
Ownership rules:
|
||||
|
||||
1. `docs/skills/` is owned exclusively by `SkillsDirectoryProvider` at runtime.
|
||||
2. `docs/prompts/` owns prompt prose; Python components own prompt metadata and argument schemas.
|
||||
2. Each file under `docs/prompts/` owns its prompt metadata, argument schema, and prose.
|
||||
3. The docs registry owns only general Markdown resources and explicitly excludes skills.
|
||||
4. `site/` is generated output.
|
||||
5. The deleted custom `catalog/` package is not part of the runtime.
|
||||
@@ -49,7 +49,7 @@ Ownership rules:
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Packaged Skills] --> B[SkillsDirectoryProvider]
|
||||
C[Prompt Components] --> D[FileSystemProvider]
|
||||
C[Packaged Prompt Markdown] --> D[Markdown Prompt Provider]
|
||||
E[Packaged Markdown] --> F[Docs Registry]
|
||||
B --> G[FastMCP]
|
||||
D --> G
|
||||
@@ -63,14 +63,14 @@ flowchart TD
|
||||
Runtime guarantees:
|
||||
|
||||
1. Providers are installed before serving requests.
|
||||
2. Production provider discovery uses `reload=False`.
|
||||
2. Prompt discovery rescans authored files on each list and get request.
|
||||
3. Duplicate components fail according to FastMCP's configured duplicate policy.
|
||||
4. Skills and prompts use native FastMCP component surfaces.
|
||||
5. General docs path parsing rejects traversal, backslashes, non-Markdown paths, and the skill namespace.
|
||||
|
||||
## Build And Publish Flow
|
||||
|
||||
1. Author Markdown under `docs/` and typed prompts under `src/personal_mcp/prompts/components/`.
|
||||
1. Author prompt definitions and prose under `docs/prompts/`.
|
||||
2. Run `uv run zensical build` to produce `site/`.
|
||||
3. Build the wheel, which packages the authored docs under `personal_mcp/docs/`.
|
||||
4. Start the app and serve MCP plus the static site.
|
||||
@@ -81,7 +81,7 @@ No runtime Markdown-to-HTML conversion occurs.
|
||||
|
||||
1. `docs/skills/<skill-id>/SKILL.md` maps to `skill://<skill-id>/SKILL.md`.
|
||||
2. Skill supporting files map to `skill://<skill-id>/<path>`.
|
||||
3. Typed prompt components map to native MCP prompt names.
|
||||
3. Declarative prompt documents map to native MCP prompt names.
|
||||
4. General `docs/<path>.md` maps to `resource://docs/{path*}`.
|
||||
|
||||
The server publishes no tool projections of resources or prompts.
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
---
|
||||
icon: lucide/messages-square
|
||||
prompt:
|
||||
version: "1.0.0"
|
||||
description: Provide a practical checklist and baseline template for authoring docs-first MCP modules and repository-specific Copilot instruction shims.
|
||||
tags: [authoring, mcp, fastmcp, copilot, prompts, scaffolding]
|
||||
arguments:
|
||||
artifact_type:
|
||||
description: Artifact type to create.
|
||||
required: true
|
||||
choices: [skill, prompt, shim]
|
||||
artifact_id:
|
||||
description: Lowercase kebab-case id for the module or shim.
|
||||
required: true
|
||||
goal:
|
||||
description: One-sentence capability statement.
|
||||
required: true
|
||||
scope_glob:
|
||||
description: Optional applyTo glob for shim outputs.
|
||||
required: false
|
||||
---
|
||||
|
||||
# Authoring Bootstrap
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
---
|
||||
icon: lucide/messages-square
|
||||
prompt:
|
||||
version: "1.0.0"
|
||||
description: Research established patterns and design a high-level architecture for a new app or library with explicit tradeoffs and test strategy.
|
||||
tags: [architecture, planning, greenfield, design, testing, prompts]
|
||||
arguments:
|
||||
scope_type:
|
||||
description: Scope type to design.
|
||||
required: true
|
||||
choices: [app, library]
|
||||
intent_document:
|
||||
description: Optional full document describing goals and context.
|
||||
required: false
|
||||
problem_domain:
|
||||
description: Problem domain and business goal.
|
||||
required: false
|
||||
constraints:
|
||||
description: Runtime, deployment, and non-functional constraints.
|
||||
required: false
|
||||
---
|
||||
|
||||
# Greenfield Architecture Planner
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
---
|
||||
icon: lucide/messages-square
|
||||
prompt:
|
||||
version: "1.1.0"
|
||||
description: Create a responsive sample page layout for a user-supplied domain and return paste-ready HTML and CSS for JSFiddle.
|
||||
tags: [frontend, html, css, jsfiddle, layout, prototyping, prompts]
|
||||
arguments:
|
||||
domain:
|
||||
description: Product, service, organization, or subject represented by the page.
|
||||
required: true
|
||||
layout_brief:
|
||||
description: Optional page type, sections, priorities, or visual constraints.
|
||||
required: false
|
||||
---
|
||||
|
||||
# JSFiddle Page Layout
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
---
|
||||
icon: lucide/messages-square
|
||||
prompt:
|
||||
version: "1.0.0"
|
||||
description: Create one repository-specific thin shim instruction file that binds a file scope to a user-selected Personal MCP skill resource.
|
||||
tags: [copilot, mcp, instructions, shims, prompts]
|
||||
arguments:
|
||||
apply_to_glob:
|
||||
description: File glob scope for the shim applyTo field.
|
||||
required: true
|
||||
primary_skill_resource:
|
||||
description: Primary native skill:// resource URI.
|
||||
required: true
|
||||
shim_title:
|
||||
description: Optional human-readable instruction shim name.
|
||||
required: false
|
||||
companion_docs_page:
|
||||
description: Optional relative companion documentation link.
|
||||
required: false
|
||||
---
|
||||
|
||||
# MCP Consumer Repository Shim
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
---
|
||||
icon: lucide/messages-square
|
||||
prompt:
|
||||
version: "1.0.0"
|
||||
description: Extract a user-selected component from a JSFiddle page layout and implement it as a reusable NiceGUI render function.
|
||||
tags: [nicegui, components, frontend, refactoring, jsfiddle, prompts]
|
||||
arguments:
|
||||
component:
|
||||
description: Visible label, semantic role, or selector identifying the component.
|
||||
required: true
|
||||
source_layout:
|
||||
description: Optional source HTML and CSS.
|
||||
required: false
|
||||
target_location:
|
||||
description: Optional target NiceGUI page, module, or package.
|
||||
required: false
|
||||
behavior_requirements:
|
||||
description: Optional interactions, state, callbacks, or variations.
|
||||
required: false
|
||||
---
|
||||
|
||||
# NiceGUI Component Extraction
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
---
|
||||
icon: lucide/messages-square
|
||||
prompt:
|
||||
version: "1.0.0"
|
||||
description: Fill scaffolded pytest methods with assertions, fixtures, and minimal test data while preserving reviewed structure.
|
||||
tags: [pytest, testing, scaffolding, prompts]
|
||||
arguments:
|
||||
target_files:
|
||||
description: Target test file paths under tests/.
|
||||
required: true
|
||||
stack:
|
||||
description: Runtime stack type for fixture and marker choices.
|
||||
required: true
|
||||
choices: [pure-python, fastapi, sqlalchemy-sync, sqlalchemy-async, mixed]
|
||||
strategy:
|
||||
description: Optional minimal or comprehensive implementation preference.
|
||||
required: false
|
||||
marker_lane:
|
||||
description: Optional pytest marker lane.
|
||||
required: false
|
||||
---
|
||||
|
||||
# Pytest Fill Scaffold
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
---
|
||||
icon: lucide/messages-square
|
||||
prompt:
|
||||
version: "1.0.0"
|
||||
description: Plan and optionally scaffold pytest file and class structure for selected Python modules.
|
||||
tags: [pytest, testing, scaffolding, prompts]
|
||||
arguments:
|
||||
target_modules:
|
||||
description: Target module paths under src/.
|
||||
required: true
|
||||
mode:
|
||||
description: Whether to plan only or create scaffold files.
|
||||
required: true
|
||||
choices: [plan-only, scaffold]
|
||||
path_strategy:
|
||||
description: Optional src-to-tests path mapping preference.
|
||||
required: false
|
||||
naming_style:
|
||||
description: Optional concise test naming preference.
|
||||
required: false
|
||||
---
|
||||
|
||||
# Pytest Scaffold
|
||||
|
||||
@@ -6,6 +6,7 @@ dependencies = [
|
||||
"fastapi>=0.133.0",
|
||||
"fastmcp==4.0.0b1",
|
||||
"pydantic-settings>=2",
|
||||
"pyyaml>=6.0.2",
|
||||
"python-json-logger>=4",
|
||||
"uvicorn[standard]>=0.34.0",
|
||||
"zensical>=0.0.45",
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
from typing import Annotated
|
||||
from typing import Literal
|
||||
|
||||
from fastmcp.prompts import prompt
|
||||
from pydantic import Field
|
||||
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
|
||||
@prompt(
|
||||
name="authoring",
|
||||
version="1.0.0",
|
||||
description=(
|
||||
"Provide a practical checklist and baseline template for authoring docs-first MCP modules and "
|
||||
"repository-specific Copilot instruction shims."
|
||||
),
|
||||
tags={"authoring", "mcp", "fastmcp", "copilot", "prompts", "scaffolding"},
|
||||
)
|
||||
def authoring(
|
||||
artifact_type: Annotated[
|
||||
Literal["skill", "prompt", "shim"],
|
||||
Field(description="Artifact type to create."),
|
||||
],
|
||||
artifact_id: Annotated[
|
||||
str,
|
||||
Field(description="Lowercase kebab-case id for the module or shim."),
|
||||
],
|
||||
goal: Annotated[
|
||||
str,
|
||||
Field(description="One-sentence capability statement."),
|
||||
],
|
||||
scope_glob: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional applyTo glob for shim outputs."),
|
||||
] = None,
|
||||
) -> str:
|
||||
return render_prompt(
|
||||
"authoring",
|
||||
{
|
||||
"artifact_type": artifact_type,
|
||||
"artifact_id": artifact_id,
|
||||
"goal": goal,
|
||||
"scope_glob": scope_glob,
|
||||
},
|
||||
)
|
||||
@@ -1,45 +0,0 @@
|
||||
from typing import Annotated
|
||||
from typing import Literal
|
||||
|
||||
from fastmcp.prompts import prompt
|
||||
from pydantic import Field
|
||||
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
|
||||
@prompt(
|
||||
name="greenfield-architecture",
|
||||
version="1.0.0",
|
||||
description=(
|
||||
"Research established patterns and design a high-level architecture for a "
|
||||
"new app or library with explicit tradeoffs and test strategy."
|
||||
),
|
||||
tags={"architecture", "planning", "greenfield", "design", "testing", "prompts"},
|
||||
)
|
||||
def greenfield_architecture(
|
||||
scope_type: Annotated[
|
||||
Literal["app", "library"],
|
||||
Field(description="Scope type to design."),
|
||||
],
|
||||
intent_document: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional full document describing goals and context."),
|
||||
] = None,
|
||||
problem_domain: Annotated[
|
||||
str | None,
|
||||
Field(description="Problem domain and business goal."),
|
||||
] = None,
|
||||
constraints: Annotated[
|
||||
str | None,
|
||||
Field(description="Runtime, deployment, and non-functional constraints."),
|
||||
] = None,
|
||||
) -> str:
|
||||
return render_prompt(
|
||||
"greenfield-architecture",
|
||||
{
|
||||
"scope_type": scope_type,
|
||||
"intent_document": intent_document,
|
||||
"problem_domain": problem_domain,
|
||||
"constraints": constraints,
|
||||
},
|
||||
)
|
||||
@@ -1,31 +0,0 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastmcp.prompts import prompt
|
||||
from pydantic import Field
|
||||
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
|
||||
@prompt(
|
||||
name="jsfiddle-page-layout",
|
||||
version="1.1.0",
|
||||
description=(
|
||||
"Create a responsive sample page layout for a user-supplied domain and return paste-ready HTML and CSS "
|
||||
"for JSFiddle."
|
||||
),
|
||||
tags={"frontend", "html", "css", "jsfiddle", "layout", "prototyping", "prompts"},
|
||||
)
|
||||
def jsfiddle_page_layout(
|
||||
domain: Annotated[
|
||||
str,
|
||||
Field(description="Product, service, organization, or subject represented by the page."),
|
||||
],
|
||||
layout_brief: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional page type, sections, priorities, or visual constraints."),
|
||||
] = None,
|
||||
) -> str:
|
||||
return render_prompt(
|
||||
"jsfiddle-page-layout",
|
||||
{"domain": domain, "layout_brief": layout_brief},
|
||||
)
|
||||
@@ -1,44 +0,0 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastmcp.prompts import prompt
|
||||
from pydantic import Field
|
||||
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
|
||||
@prompt(
|
||||
name="mcp-consumer-repo-shim",
|
||||
version="1.0.0",
|
||||
description=(
|
||||
"Create one repository-specific thin shim instruction file that binds a file scope to a user-selected "
|
||||
"Personal MCP skill resource."
|
||||
),
|
||||
tags={"copilot", "mcp", "instructions", "shims", "prompts"},
|
||||
)
|
||||
def mcp_consumer_repo_shim(
|
||||
apply_to_glob: Annotated[
|
||||
str,
|
||||
Field(description="File glob scope for the shim applyTo field."),
|
||||
],
|
||||
primary_skill_resource: Annotated[
|
||||
str,
|
||||
Field(description="Primary native skill:// resource URI."),
|
||||
],
|
||||
shim_title: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional human-readable instruction shim name."),
|
||||
] = None,
|
||||
companion_docs_page: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional relative companion documentation link."),
|
||||
] = None,
|
||||
) -> str:
|
||||
return render_prompt(
|
||||
"mcp-consumer-repo-shim",
|
||||
{
|
||||
"apply_to_glob": apply_to_glob,
|
||||
"primary_skill_resource": primary_skill_resource,
|
||||
"shim_title": shim_title,
|
||||
"companion_docs_page": companion_docs_page,
|
||||
},
|
||||
)
|
||||
@@ -1,44 +0,0 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastmcp.prompts import prompt
|
||||
from pydantic import Field
|
||||
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
|
||||
@prompt(
|
||||
name="nicegui-component-extraction",
|
||||
version="1.0.0",
|
||||
description=(
|
||||
"Extract a user-selected component from a JSFiddle page layout and implement it as a reusable NiceGUI "
|
||||
"render function."
|
||||
),
|
||||
tags={"nicegui", "components", "frontend", "refactoring", "jsfiddle", "prompts"},
|
||||
)
|
||||
def nicegui_component_extraction(
|
||||
component: Annotated[
|
||||
str,
|
||||
Field(description="Visible label, semantic role, or selector identifying the component."),
|
||||
],
|
||||
source_layout: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional source HTML and CSS."),
|
||||
] = None,
|
||||
target_location: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional target NiceGUI page, module, or package."),
|
||||
] = None,
|
||||
behavior_requirements: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional interactions, state, callbacks, or variations."),
|
||||
] = None,
|
||||
) -> str:
|
||||
return render_prompt(
|
||||
"nicegui-component-extraction",
|
||||
{
|
||||
"component": component,
|
||||
"source_layout": source_layout,
|
||||
"target_location": target_location,
|
||||
"behavior_requirements": behavior_requirements,
|
||||
},
|
||||
)
|
||||
@@ -1,45 +0,0 @@
|
||||
from typing import Annotated
|
||||
from typing import Literal
|
||||
|
||||
from fastmcp.prompts import prompt
|
||||
from pydantic import Field
|
||||
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
|
||||
@prompt(
|
||||
name="pytest-fill-scaffold",
|
||||
version="1.0.0",
|
||||
description=(
|
||||
"Fill scaffolded pytest methods with assertions, fixtures, and minimal test data while preserving reviewed "
|
||||
"structure."
|
||||
),
|
||||
tags={"pytest", "testing", "scaffolding", "prompts"},
|
||||
)
|
||||
def pytest_fill_scaffold(
|
||||
target_files: Annotated[
|
||||
str,
|
||||
Field(description="Target test file paths under tests/."),
|
||||
],
|
||||
stack: Annotated[
|
||||
Literal["pure-python", "fastapi", "sqlalchemy-sync", "sqlalchemy-async", "mixed"],
|
||||
Field(description="Runtime stack type for fixture and marker choices."),
|
||||
],
|
||||
strategy: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional minimal or comprehensive implementation preference."),
|
||||
] = None,
|
||||
marker_lane: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional pytest marker lane."),
|
||||
] = None,
|
||||
) -> str:
|
||||
return render_prompt(
|
||||
"pytest-fill-scaffold",
|
||||
{
|
||||
"target_files": target_files,
|
||||
"stack": stack,
|
||||
"strategy": strategy,
|
||||
"marker_lane": marker_lane,
|
||||
},
|
||||
)
|
||||
@@ -1,42 +0,0 @@
|
||||
from typing import Annotated
|
||||
from typing import Literal
|
||||
|
||||
from fastmcp.prompts import prompt
|
||||
from pydantic import Field
|
||||
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
|
||||
@prompt(
|
||||
name="pytest-scaffold",
|
||||
version="1.0.0",
|
||||
description="Plan and optionally scaffold pytest file and class structure for selected Python modules.",
|
||||
tags={"pytest", "testing", "scaffolding", "prompts"},
|
||||
)
|
||||
def pytest_scaffold(
|
||||
target_modules: Annotated[
|
||||
str,
|
||||
Field(description="Target module paths under src/."),
|
||||
],
|
||||
mode: Annotated[
|
||||
Literal["plan-only", "scaffold"],
|
||||
Field(description="Whether to plan only or create scaffold files."),
|
||||
],
|
||||
path_strategy: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional src-to-tests path mapping preference."),
|
||||
] = None,
|
||||
naming_style: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional concise test naming preference."),
|
||||
] = None,
|
||||
) -> str:
|
||||
return render_prompt(
|
||||
"pytest-scaffold",
|
||||
{
|
||||
"target_modules": target_modules,
|
||||
"mode": mode,
|
||||
"path_strategy": path_strategy,
|
||||
"naming_style": naming_style,
|
||||
},
|
||||
)
|
||||
@@ -1,29 +1,66 @@
|
||||
import re
|
||||
from importlib.resources import files
|
||||
from importlib.resources.abc import Traversable
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import ValidationError
|
||||
|
||||
from personal_mcp.prompts.models import PromptDefinition
|
||||
from personal_mcp.prompts.models import PromptMetadata
|
||||
|
||||
_PROMPT_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
||||
_FRONTMATTER_RE = re.compile(r"\A---\r?\n.*?\r?\n---\r?\n?", re.DOTALL)
|
||||
_FRONTMATTER_RE = re.compile(r"\A---\r?\n(?P<yaml>.*?)\r?\n---(?:\r?\n|$)", re.DOTALL)
|
||||
_PLACEHOLDER_RE = re.compile(r"\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}")
|
||||
|
||||
|
||||
def render_prompt(prompt_id: str, arguments: dict[str, Any]) -> str:
|
||||
def load_prompt_definition(prompt_id: str, resource: Traversable) -> PromptDefinition:
|
||||
if not _PROMPT_ID_RE.fullmatch(prompt_id):
|
||||
raise ValueError("prompt_id must be lowercase kebab-case")
|
||||
|
||||
resource = files("personal_mcp").joinpath("docs", "prompts", prompt_id, "PROMPT.md")
|
||||
raise ValueError(f"prompt id must be lowercase kebab-case: {prompt_id!r}")
|
||||
if not resource.is_file():
|
||||
raise FileNotFoundError(f"prompt document does not exist: {prompt_id}")
|
||||
|
||||
content = _FRONTMATTER_RE.sub("", resource.read_text(encoding="utf-8"), count=1)
|
||||
placeholders = set(_PLACEHOLDER_RE.findall(content))
|
||||
argument_names = set(arguments)
|
||||
raw = resource.read_text(encoding="utf-8")
|
||||
match = _FRONTMATTER_RE.match(raw)
|
||||
if match is None:
|
||||
raise ValueError(f"prompt document must start with YAML frontmatter: {prompt_id}")
|
||||
|
||||
try:
|
||||
frontmatter = yaml.safe_load(match.group("yaml"))
|
||||
except yaml.YAMLError as error:
|
||||
raise ValueError(f"invalid YAML frontmatter for prompt {prompt_id!r}: {error}") from error
|
||||
if not isinstance(frontmatter, dict):
|
||||
raise TypeError(f"prompt frontmatter must be a mapping: {prompt_id}")
|
||||
if "prompt" not in frontmatter:
|
||||
raise ValueError(f"prompt frontmatter is missing the 'prompt' block: {prompt_id}")
|
||||
|
||||
try:
|
||||
metadata = PromptMetadata.model_validate(frontmatter["prompt"])
|
||||
except ValidationError as error:
|
||||
raise ValueError(f"invalid metadata for prompt {prompt_id!r}: {error}") from error
|
||||
body = raw[match.end() :]
|
||||
placeholders = set(_PLACEHOLDER_RE.findall(body))
|
||||
argument_names = set(metadata.arguments)
|
||||
if placeholders != argument_names:
|
||||
missing = sorted(argument_names - placeholders)
|
||||
unknown = sorted(placeholders - argument_names)
|
||||
raise ValueError(
|
||||
f"prompt placeholders do not match arguments for {prompt_id!r}; missing={missing}, unknown={unknown}"
|
||||
)
|
||||
|
||||
return PromptDefinition(prompt_id=prompt_id, metadata=metadata, body=body)
|
||||
|
||||
|
||||
def render_prompt(prompt_id: str, arguments: dict[str, Any]) -> str:
|
||||
resource = files("personal_mcp").joinpath("docs", "prompts", prompt_id, "PROMPT.md")
|
||||
definition = load_prompt_definition(prompt_id, resource)
|
||||
argument_names = set(definition.metadata.arguments)
|
||||
if set(arguments) != argument_names:
|
||||
missing = sorted(argument_names - set(arguments))
|
||||
unknown = sorted(set(arguments) - argument_names)
|
||||
raise ValueError(f"prompt placeholders do not match arguments; missing={missing}, unknown={unknown}")
|
||||
|
||||
rendered = content
|
||||
rendered = definition.body
|
||||
for name, value in arguments.items():
|
||||
replacement = "Not provided" if value is None else str(value)
|
||||
rendered = rendered.replace(f"{{{{{name}}}}}", replacement)
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import re
|
||||
from typing import Self
|
||||
|
||||
from fastmcp.exceptions import PromptError
|
||||
from fastmcp.prompts import Prompt
|
||||
from fastmcp.prompts import PromptArgument
|
||||
from pydantic import BaseModel
|
||||
from pydantic import ConfigDict
|
||||
from pydantic import Field
|
||||
from pydantic import model_validator
|
||||
|
||||
_ARGUMENT_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||
|
||||
|
||||
class PromptArgumentDefinition(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
description: str = Field(min_length=1)
|
||||
required: bool
|
||||
choices: tuple[str, ...] | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_choices(self) -> Self:
|
||||
if self.choices is None:
|
||||
return self
|
||||
if not self.choices:
|
||||
raise ValueError("choices must contain at least one value")
|
||||
if any(not choice for choice in self.choices):
|
||||
raise ValueError("choices must not contain empty values")
|
||||
if len(set(self.choices)) != len(self.choices):
|
||||
raise ValueError("choices must not contain duplicate values")
|
||||
return self
|
||||
|
||||
|
||||
class PromptMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
version: str = Field(min_length=1)
|
||||
description: str = Field(min_length=1)
|
||||
tags: frozenset[str] = Field(min_length=1)
|
||||
arguments: dict[str, PromptArgumentDefinition]
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_arguments(self) -> Self:
|
||||
invalid_names = [name for name in self.arguments if not _ARGUMENT_NAME_RE.fullmatch(name)]
|
||||
if invalid_names:
|
||||
raise ValueError(f"argument names must be valid identifiers: {invalid_names}")
|
||||
return self
|
||||
|
||||
|
||||
class PromptDefinition(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
prompt_id: str
|
||||
metadata: PromptMetadata
|
||||
body: str
|
||||
|
||||
|
||||
class MarkdownPrompt(Prompt):
|
||||
body: str = Field(exclude=True)
|
||||
definitions: dict[str, PromptArgumentDefinition] = Field(exclude=True)
|
||||
|
||||
@classmethod
|
||||
def from_definition(cls, definition: PromptDefinition) -> Self:
|
||||
metadata = definition.metadata
|
||||
return cls(
|
||||
name=definition.prompt_id,
|
||||
version=metadata.version,
|
||||
description=metadata.description,
|
||||
tags=set(metadata.tags),
|
||||
arguments=[
|
||||
PromptArgument(
|
||||
name=name,
|
||||
description=(
|
||||
argument.description
|
||||
if argument.choices is None
|
||||
else f"{argument.description}\n\nAccepted values: {', '.join(argument.choices)}."
|
||||
),
|
||||
required=argument.required,
|
||||
)
|
||||
for name, argument in metadata.arguments.items()
|
||||
],
|
||||
body=definition.body,
|
||||
definitions=metadata.arguments,
|
||||
)
|
||||
|
||||
async def render(self, arguments: dict[str, object] | None = None) -> str:
|
||||
provided = arguments or {}
|
||||
declared_names = set(self.definitions)
|
||||
unknown = sorted(set(provided) - declared_names)
|
||||
if unknown:
|
||||
raise PromptError(f"Unknown arguments for prompt {self.name!r}: {unknown}")
|
||||
|
||||
missing = sorted(
|
||||
name for name, definition in self.definitions.items() if definition.required and name not in provided
|
||||
)
|
||||
if missing:
|
||||
raise PromptError(f"Missing required arguments for prompt {self.name!r}: {missing}")
|
||||
|
||||
normalized: dict[str, object] = {}
|
||||
for name, definition in self.definitions.items():
|
||||
value = provided.get(name)
|
||||
if value is not None and not isinstance(value, str):
|
||||
raise PromptError(f"Argument {name!r} for prompt {self.name!r} must be a string")
|
||||
if value is not None and definition.choices is not None and value not in definition.choices:
|
||||
raise PromptError(
|
||||
f"Argument {name!r} for prompt {self.name!r} must be one of {list(definition.choices)}"
|
||||
)
|
||||
normalized[name] = value
|
||||
|
||||
rendered = self.body
|
||||
for name, value in normalized.items():
|
||||
replacement = "Not provided" if value is None else str(value)
|
||||
rendered = rendered.replace(f"{{{{{name}}}}}", replacement)
|
||||
return rendered
|
||||
@@ -1,10 +1,34 @@
|
||||
from pathlib import Path
|
||||
from collections.abc import Sequence
|
||||
from importlib.resources import files
|
||||
from importlib.resources.abc import Traversable
|
||||
|
||||
from fastmcp.server.providers import FileSystemProvider
|
||||
from fastmcp.prompts import Prompt
|
||||
from fastmcp.server.providers import Provider
|
||||
|
||||
from personal_mcp.prompts.content import load_prompt_definition
|
||||
from personal_mcp.prompts.models import MarkdownPrompt
|
||||
|
||||
|
||||
def create_prompts_provider() -> FileSystemProvider:
|
||||
components_root = Path(__file__).parent / "components"
|
||||
if not components_root.is_dir():
|
||||
raise FileNotFoundError(f"prompt components root does not exist or is not a directory: {components_root}")
|
||||
return FileSystemProvider(root=components_root, reload=False)
|
||||
class MarkdownPromptsProvider(Provider):
|
||||
def __init__(self, root: Traversable) -> None:
|
||||
super().__init__()
|
||||
if not root.is_dir():
|
||||
raise FileNotFoundError(f"prompts root does not exist or is not a directory: {root}")
|
||||
self._root = root
|
||||
|
||||
async def _list_prompts(self) -> Sequence[Prompt]:
|
||||
prompts: list[Prompt] = []
|
||||
for directory in sorted(self._root.iterdir(), key=lambda child: child.name):
|
||||
if not directory.is_dir():
|
||||
continue
|
||||
document = directory.joinpath("PROMPT.md")
|
||||
if not document.is_file():
|
||||
continue
|
||||
definition = load_prompt_definition(directory.name, document)
|
||||
prompts.append(MarkdownPrompt.from_definition(definition))
|
||||
return prompts
|
||||
|
||||
|
||||
def create_prompts_provider(root: Traversable | None = None) -> MarkdownPromptsProvider:
|
||||
prompts_root = root or files("personal_mcp").joinpath("docs", "prompts")
|
||||
return MarkdownPromptsProvider(prompts_root)
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"""FastMCP provider composition for personal MCP skills."""
|
||||
|
||||
from .provider import create_skills_provider
|
||||
|
||||
__all__ = ["create_skills_provider"]
|
||||
@@ -1,5 +1,8 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.prompts.content import load_prompt_definition
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
@@ -23,3 +26,32 @@ class TestPromptContentRenderer:
|
||||
def test_rejects_invalid_prompt_id(self) -> None:
|
||||
with pytest.raises(ValueError, match="lowercase kebab-case"):
|
||||
render_prompt("../outside", {})
|
||||
|
||||
def test_rejects_missing_prompt_metadata(self, tmp_path: Path) -> None:
|
||||
document = tmp_path / "PROMPT.md"
|
||||
document.write_text("---\nicon: lucide/messages-square\n---\n\n# Body\n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="missing the 'prompt' block"):
|
||||
load_prompt_definition("demo", document)
|
||||
|
||||
def test_rejects_unknown_prompt_metadata(self, tmp_path: Path) -> None:
|
||||
document = tmp_path / "PROMPT.md"
|
||||
document.write_text(
|
||||
"---\nprompt: {version: '1', description: Demo, tags: [demo], arguments: {}, unknown: true}\n"
|
||||
"---\n\n# Body\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Extra inputs are not permitted"):
|
||||
load_prompt_definition("demo", document)
|
||||
|
||||
def test_rejects_declared_placeholder_drift(self, tmp_path: Path) -> None:
|
||||
document = tmp_path / "PROMPT.md"
|
||||
document.write_text(
|
||||
"---\nprompt: {version: '1', description: Demo, tags: [demo], arguments: "
|
||||
"{topic: {description: Topic, required: true}}}\n---\n\n# Body\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="placeholders do not match arguments"):
|
||||
load_prompt_definition("demo", document)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.exceptions import PromptError
|
||||
|
||||
from personal_mcp.prompts import create_prompts_provider
|
||||
|
||||
@@ -17,7 +20,20 @@ EXPECTED_PROMPTS = {
|
||||
}
|
||||
|
||||
|
||||
class TestPromptFileSystemProvider:
|
||||
def write_prompt(document: Path, *, description: str, heading: str = "Demo") -> None:
|
||||
document.parent.mkdir(parents=True, exist_ok=True)
|
||||
document.write_text(
|
||||
"---\n"
|
||||
f"prompt: {{version: '1.0.0', description: {description!r}, tags: [demo], arguments: "
|
||||
"{kind: {description: 'Kind to render.', required: true, choices: [first, second]}, "
|
||||
"note: {description: 'Optional note.', required: false}}}\n"
|
||||
"---\n\n"
|
||||
f"# {heading}\n\nKind: {{{{kind}}}}\n\nNote: {{{{note}}}}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class TestMarkdownPromptsProvider:
|
||||
@pytest.mark.asyncio
|
||||
async def test_discovers_exact_authored_set(self) -> None:
|
||||
mcp = FastMCP("prompts-test")
|
||||
@@ -47,6 +63,59 @@ class TestPromptFileSystemProvider:
|
||||
)
|
||||
|
||||
required = {argument.name for argument in authoring.arguments or [] if argument.required}
|
||||
artifact_type = next(argument for argument in authoring.arguments or [] if argument.name == "artifact_type")
|
||||
assert required == {"artifact_type", "artifact_id", "goal"}
|
||||
assert artifact_type.description == "Artifact type to create.\n\nAccepted values: skill, prompt, shim."
|
||||
assert result.messages
|
||||
assert "`artifact_id`: demo-skill" in result.messages[0].content.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enforces_required_arguments_and_choices(self) -> None:
|
||||
provider = create_prompts_provider()
|
||||
prompt = await provider.get_prompt("authoring")
|
||||
|
||||
assert prompt is not None
|
||||
with pytest.raises(PromptError, match="Missing required arguments"):
|
||||
await prompt.render({"artifact_type": "skill"})
|
||||
with pytest.raises(PromptError, match="must be one of"):
|
||||
await prompt.render(
|
||||
{
|
||||
"artifact_type": "unsupported",
|
||||
"artifact_id": "demo-skill",
|
||||
"goal": "Demonstrate validation.",
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_live_loads_edits_without_python_components(self, tmp_path: Path) -> None:
|
||||
prompts_root = tmp_path / "prompts"
|
||||
prompts_root.mkdir()
|
||||
provider = create_prompts_provider(prompts_root)
|
||||
mcp = FastMCP("prompts-test")
|
||||
mcp.add_provider(provider)
|
||||
|
||||
async with Client(mcp) as client:
|
||||
assert await client.list_prompts() == []
|
||||
|
||||
document = prompts_root / "dynamic-demo" / "PROMPT.md"
|
||||
write_prompt(document, description="Initial description")
|
||||
|
||||
prompts = await client.list_prompts()
|
||||
assert [prompt.name for prompt in prompts] == ["dynamic-demo"]
|
||||
assert prompts[0].description == "Initial description"
|
||||
result = await client.get_prompt("dynamic-demo", {"kind": "first"})
|
||||
assert "# Demo" in result.messages[0].content.text
|
||||
assert "Note: Not provided" in result.messages[0].content.text
|
||||
|
||||
write_prompt(document, description="Updated description", heading="Updated")
|
||||
|
||||
prompts = await client.list_prompts()
|
||||
assert prompts[0].description == "Updated description"
|
||||
result = await client.get_prompt("dynamic-demo", {"kind": "second", "note": "ready"})
|
||||
assert "# Updated" in result.messages[0].content.text
|
||||
assert "Note: ready" in result.messages[0].content.text
|
||||
|
||||
document.unlink()
|
||||
document.parent.rmdir()
|
||||
|
||||
assert await client.list_prompts() == []
|
||||
|
||||
@@ -1171,6 +1171,7 @@ dependencies = [
|
||||
{ name = "fastmcp" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "python-json-logger" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
{ name = "zensical" },
|
||||
]
|
||||
@@ -1196,6 +1197,7 @@ requires-dist = [
|
||||
{ name = "fastmcp", specifier = "==4.0.0b1" },
|
||||
{ name = "pydantic-settings", specifier = ">=2" },
|
||||
{ name = "python-json-logger", specifier = ">=4" },
|
||||
{ name = "pyyaml", specifier = ">=6.0.2" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" },
|
||||
{ name = "zensical", specifier = ">=0.0.45" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user