prompt markdown
This commit is contained in:
@@ -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"]
|
||||
Reference in New Issue
Block a user