install_skill_from_markdown
Install a new skill into the MCP server by providing markdown with front matter and instructions. Optionally activate it and set the source.
Instructions
Install a skill from markdown front matter and instructions.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| markdown | Yes | ||
| source | No | generated | |
| activate | No |
Implementation Reference
- The core implementation of install_skill_from_markdown in SkillStore. Parses markdown, validates compatibility, backs up existing skill, writes the skill file, creates a registry record with metadata, and saves the registry.
def install_skill_from_markdown( self, markdown: str, *, source: str, source_type: str, activate: bool = True, ) -> dict[str, Any]: document = self._parse_skill(markdown) self._ensure_compatible(document.min_server_version) registry = self._load_registry() target_path = self.installed_dir / f"{document.skill_id}.md" backup_path = None if target_path.exists(): timestamp = self._timestamp_slug() backup_path = self.backups_dir / f"{document.skill_id}-{timestamp}.md" shutil.copy2(target_path, backup_path) target_path.write_text(self._serialize_skill(document), encoding="utf-8") record = { "id": document.skill_id, "name": document.name, "version": document.version, "description": document.description, "capabilities": document.capabilities, "min_server_version": document.min_server_version, "active": activate, "source": source, "source_type": source_type, "installed_at": self._timestamp(), "checksum": self._checksum(target_path.read_text(encoding="utf-8")), "path": str(target_path), "backup_path": str(backup_path) if backup_path else None, } registry[document.skill_id] = record self._save_registry(registry) return record - The MCP tool handler for 'install_skill_from_markdown'. This is the @mcp.tool() decorated function that delegates to skill_store.install_skill_from_markdown with source='generated' and source_type='generated'.
def install_skill_from_markdown( markdown: str, source: str = "generated", activate: bool = True, ) -> dict: """Install a skill from markdown front matter and instructions.""" return skill_store.install_skill_from_markdown( markdown, source=source, source_type="generated", activate=activate, ) - src/friday_mcp_server/tools/__init__.py:6-12 (registration)The register_all_tools function that calls skills.register() to register all skill tools including install_skill_from_markdown.
def register_all_tools(mcp, *, config, skill_store) -> None: system.register(mcp, config=config) utils.register(mcp) web.register(mcp, config=config) workspace.register(mcp, config=config) skills.register(mcp, skill_store=skill_store) - src/friday_mcp_server/tools/skills.py:10-80 (registration)The register() function that registers all skill tools (including install_skill_from_markdown) via @mcp.tool() decorators.
def register(mcp, *, skill_store) -> None: @mcp.tool() def list_skills(active_only: bool = False) -> list[dict]: """List installed skills and their activation state.""" return skill_store.list_skills(active_only=active_only) @mcp.tool() def get_skill(skill_id: str) -> dict: """Get the full installed content and metadata for a skill.""" return skill_store.get_skill(skill_id) @mcp.tool() def validate_skill_markdown(markdown: str) -> dict: """Validate a candidate skill document before installation.""" return skill_store.validate_skill_markdown(markdown) @mcp.tool() def install_skill_from_markdown( markdown: str, source: str = "generated", activate: bool = True, ) -> dict: """Install a skill from markdown front matter and instructions.""" return skill_store.install_skill_from_markdown( markdown, source=source, source_type="generated", activate=activate, ) @mcp.tool() async def install_skill_from_url(url: str, activate: bool = True) -> dict: """Download, validate, install, and optionally activate a skill from a URL.""" async with httpx.AsyncClient(follow_redirects=True, timeout=15.0) as client: response = await client.get(url) response.raise_for_status() return skill_store.install_skill_from_markdown( response.text, source=url, source_type="url", activate=activate, ) @mcp.tool() def activate_skill(skill_id: str) -> dict: """Activate an installed skill.""" return skill_store.activate_skill(skill_id) @mcp.tool() def deactivate_skill(skill_id: str) -> dict: """Deactivate an installed skill.""" return skill_store.deactivate_skill(skill_id) @mcp.tool() def remove_skill(skill_id: str) -> dict: """Remove a skill and keep a rollback backup.""" return skill_store.remove_skill(skill_id) @mcp.tool() def rollback_skill(skill_id: str, backup_file: str = "") -> dict: """Restore the latest or selected backup of a skill.""" return skill_store.rollback_skill(skill_id, backup_file or None) @mcp.tool() def explain_skill_error(error_text: str) -> str: """Normalize skill validation failures for the client.""" try: raise SkillError(error_text) except SkillError as exc: return str(exc) - The _parse_skill helper used by install_skill_from_markdown to parse YAML front matter and instructions from markdown into a SkillDocument.
def _parse_skill(self, markdown: str) -> SkillDocument: lines = markdown.splitlines() if not lines or lines[0].strip() != "---": raise SkillError("Skill markdown must start with YAML front matter.") closing_index = None for index in range(1, len(lines)): if lines[index].strip() == "---": closing_index = index break if closing_index is None: raise SkillError("Skill front matter is missing a closing '---' line.") front_matter = "\n".join(lines[1:closing_index]) body = "\n".join(lines[closing_index + 1 :]).strip() if not body: raise SkillError("Skill instructions cannot be empty.") metadata = yaml.safe_load(front_matter) or {} skill_id = str(metadata.get("id", "")).strip() if not SKILL_ID_RE.match(skill_id): raise SkillError( "Skill id is required and must match ^[a-z0-9][a-z0-9-]{1,63}$." ) name = str(metadata.get("name", "")).strip() version = str(metadata.get("version", "")).strip() description = str(metadata.get("description", "")).strip() min_server_version = str(metadata.get("min_server_version", __version__)).strip() capabilities = metadata.get("capabilities") or [] if not name or not version or not description: raise SkillError("Skill front matter requires name, version, and description.") if not isinstance(capabilities, list) or not all( isinstance(item, str) and item.strip() for item in capabilities ): raise SkillError("Skill capabilities must be a list of non-empty strings.") return SkillDocument( skill_id=skill_id, name=name, version=version, description=description, instructions=body, capabilities=[item.strip() for item in capabilities], min_server_version=min_server_version, )