diff --git a/.gitignore b/.gitignore index d6b130c..b445672 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,12 @@ .claude/settings.local.json + +# Local reference repos and planning docs +%TEMP%/ +TODO.md + +# Build artifacts +node_modules/ +dist/ +__pycache__/ +*.egg-info/ +.eggs/ diff --git a/examples/skills-as-resources/README.md b/examples/skills-as-resources/README.md new file mode 100644 index 0000000..f2b89b2 --- /dev/null +++ b/examples/skills-as-resources/README.md @@ -0,0 +1,168 @@ +# Skills as Resources — Reference Implementation + +> **Experimental** — This is a minimal reference implementation for evaluation by the Skills Over MCP Interest Group. Not intended for production use. + +## Pattern Overview + +This example demonstrates the **Resources approach** from [`docs/approaches.md`](../../docs/approaches.md): exposing agent skills via MCP resources using the `skill://` URI scheme. + +An MCP server scans a directory for SKILL.md files and exposes them as resources: + +| Resource | URI | MIME Type | Purpose | +| :--- | :--- | :--- | :--- | +| Index | `skill://index` | `application/json` | JSON array of all skill summaries | +| Prompt XML | `skill://prompt-xml` | `application/xml` | XML for system prompt injection | +| Skill content | `skill://{name}` | `text/markdown` | Full SKILL.md content for a specific skill | +| Document list | `skill://{name}/documents` | `application/json` | List of supplementary files (if any) | +| Document | `skill://{name}/document/{path}` | varies | Individual supplementary document | + +This is an **application-controlled** approach: the host/client decides when to read resources. See [Open Question #9](../../docs/open-questions.md) for the control model discussion. + +## How It Works + +``` +┌─────────────┐ ┌──────────────────────┐ ┌──────────────┐ +│ MCP Client │────▶│ Skills as Resources │────▶│ Skill Files │ +│ (e.g. Claude│◀────│ MCP Server │◀────│ (SKILL.md) │ +│ Code) │ └──────────────────────┘ └──────────────┘ +└─────────────┘ +``` + +1. **Startup**: Server scans the configured skills directory for `*/SKILL.md` files and supplementary documents +2. **Discovery**: Parses YAML frontmatter to extract `name` and `description` +3. **Registration**: Registers static resources for each skill, plus a `ResourceTemplate` for supplementary documents; resource descriptions include available skill names +4. **Progressive disclosure**: + - `skill://index` → Summaries only (names, descriptions, URIs) + - `skill://{name}` → Full SKILL.md content on demand + - `skill://{name}/documents` → List of supplementary files + - `skill://{name}/document/{path}` → Individual supplementary file +5. **System prompt injection**: `skill://prompt-xml` provides XML that hosts can inject into system prompts +6. **Capability declaration**: Server declares `resources.listChanged` capability (dynamic updates could be wired to a file watcher in a full implementation) + +## Implementations + +Both implementations expose the same resources with the same behavior. They share the `sample-skills/` directory as test data. + +### TypeScript + +**Prerequisites**: Node.js >= 18, npm + +```bash +cd typescript +npm install +npm run build +``` + +**Run with MCP Inspector**: +```bash +npx @modelcontextprotocol/inspector node dist/index.js ../sample-skills +``` + +**Development mode** (no build step): +```bash +npm run dev -- ../sample-skills +``` + +### Python + +**Prerequisites**: Python >= 3.10, pip (or uv) + +```bash +cd python +pip install -e . +``` + +**Run with MCP Inspector**: +```bash +npx @modelcontextprotocol/inspector -- python -m skills_as_resources.server ../sample-skills +``` + +### SDK Difference: Document Path Encoding + +The TypeScript MCP SDK supports RFC 6570 reserved expansion (`{+path}`), so document URIs use natural paths: + +``` +skill://code-review/document/references/REFERENCE.md +``` + +The Python MCP SDK uses `[^/]+` regex for all template parameters, so forward slashes in paths must be URL-encoded: + +``` +skill://code-review/document/references%2FREFERENCE.md +``` + +The SDK automatically URL-decodes the path after matching, so the handler receives the natural path in both cases. This difference is transparent to the resource handler logic. + +## Security Features + +Both implementations include: + +- **Path traversal protection** — Resolved paths are checked against the skills directory boundary using `realpathSync` (TS) / `Path.resolve()` (Python). Symlink escapes are detected. +- **Skill name validation** — Resources look up names by key in the discovered skills map. User input is never used to construct file paths. +- **Document path validation** — Paths containing `..` are rejected. All document paths are verified to be within the skills directory. +- **File size limits** — Files larger than 1MB are skipped during discovery and rejected on read. +- **Safe YAML parsing** — Python uses `yaml.safe_load()` to prevent code execution. TypeScript uses the `yaml` package which is safe by default. + +## Sample Skills + +Two sample skills are included in `sample-skills/` for testing: + +| Skill | Description | Documents | Notes | +| :--- | :--- | :--- | :--- | +| `code-review` | Structured code review methodology | `references/REFERENCE.md` | Tests document scanning and `ResourceTemplate` | +| `git-commit-review` | Review commits for quality and conventional format | None | Tests basic skill resource with no documents | + +## Key Design Decisions + +- **Resources, not tools**: Resources are application-controlled — the host/client decides when to read them. This demonstrates a fundamentally different control model than the tools approach, where the LLM decides when to invoke. See [`docs/experimental-findings.md`](../../docs/experimental-findings.md) for observations on how control model affects utilization. +- **Static resources for skills, template for documents**: Each discovered skill becomes a concrete resource visible in `resources/list`. Only supplementary document fetching uses a `ResourceTemplate`, since document paths are dynamic. +- **Progressive disclosure via URI hierarchy**: `skill://index` → `skill://{name}` → `skill://{name}/documents` → `skill://{name}/document/{path}`. Clients can fetch summaries first and load full content on demand. +- **`skill://prompt-xml` for injection**: Allows hosts to inject skill awareness into system prompts using the resources primitive, rather than embedding skill names in tool descriptions. +- **No `zod` dependency**: Unlike the tools approach, resources do not require input schemas, so the Zod dependency is not needed. + +## How This Differs from Skills as Tools + +| Aspect | Skills as Tools | Skills as Resources | +| :--- | :--- | :--- | +| Control model | Model-controlled (LLM invokes) | Application-controlled (host/client reads) | +| MCP Primitive | Tools | Resources | +| Discovery | Tool description + `list_skills` call | `resources/list` + `skill://index` | +| Loading | `read_skill(name)` tool call | `resources/read` on `skill://{name}` | +| System prompt | Via tool description embedding | Via `skill://prompt-xml` resource | +| Input validation | Zod schema on tool parameters | URI template matching | +| Supplementary files | Not demonstrated | `ResourceTemplate` for documents | + +## What This Example Intentionally Omits + +- File watching / resource subscriptions (capability is declared but not wired) +- Dynamic updates (`resources.listChanged` is declared but not triggered) +- MCP Prompts for explicit skill invocation +- GitHub sync, configuration UI +- `skill://` URI scheme registration or standardization + +## Answers to Open Question #12 + +> "Why not just use resources?" + +This implementation shows that resources **do work** for skill delivery. Key findings for evaluation: + +- **Discovery**: Skills appear in `resources/list`, making them immediately visible to any MCP-aware client +- **Progressive disclosure**: The URI hierarchy (`index` → `skill` → `documents` → `document`) provides the same layered loading as the tools approach +- **System prompt injection**: `skill://prompt-xml` provides a clean mechanism for hosts to inject skill awareness +- **Control model trade-off**: Resources are application-controlled — the host decides when/whether to read them. This may lead to lower utilization compared to model-controlled tools (see experimental findings), but gives the host more control over context management + +## Relationship to Other Approaches + +| Approach | How it differs | +| :--- | :--- | +| **1. Skills as Primitives** (SEP-2076) | Uses dedicated `skills/list` and `skills/get` protocol methods instead of resources | +| **3. Skills as Tools** (sibling example) | Uses MCP tools (model-controlled) instead of resources (application-controlled) | +| **5. Server Instructions** | Uses server instructions to point to resources instead of exposing resources directly | +| **6. Convention** | This example could become part of a documented convention pattern | + +## Inspirations and Attribution + +This reference implementation is original code inspired by patterns from: + +- **[skills-over-mcp](https://github.com/keithagroves/skills-over-mcp)** by [Keith Groves](https://github.com/keithagroves) — Resource-based skill exposure, `skill://` URI scheme, JSON index, XML prompt injection, document templates +- **[skilljack-mcp](https://github.com/olaservo/skilljack-mcp)** by [Ola Hungerford](https://github.com/olaservo) — Resource template patterns, subscription architecture, path security diff --git a/examples/skills-as-resources/python/pyproject.toml b/examples/skills-as-resources/python/pyproject.toml new file mode 100644 index 0000000..4e7d695 --- /dev/null +++ b/examples/skills-as-resources/python/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "skills-as-resources-example" +version = "0.1.0" +description = "Minimal reference implementation: Skills as MCP Resources" +requires-python = ">=3.10" +dependencies = [ + "mcp>=1.0.0", + "pyyaml>=6.0", +] +license = "Apache-2.0" + +[project.scripts] +skills-as-resources = "skills_as_resources.server:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/skills_as_resources"] diff --git a/examples/skills-as-resources/python/src/skills_as_resources/__init__.py b/examples/skills-as-resources/python/src/skills_as_resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/skills-as-resources/python/src/skills_as_resources/resource_helpers.py b/examples/skills-as-resources/python/src/skills_as_resources/resource_helpers.py new file mode 100644 index 0000000..2e46452 --- /dev/null +++ b/examples/skills-as-resources/python/src/skills_as_resources/resource_helpers.py @@ -0,0 +1,71 @@ +""" +Resource helper utilities for the Skills as Resources implementation. + +Provides XML generation for system prompt injection and MIME type mapping +for skill documents. + +Inspired by: +- skills-over-mcp by Keith Groves (https://github.com/keithagroves/skills-over-mcp) +""" + +from __future__ import annotations + +import os +from xml.sax.saxutils import escape + +from .skill_discovery import SkillMetadata + +# Map file extensions to MIME types +MIME_TYPES: dict[str, str] = { + ".md": "text/markdown", + ".txt": "text/plain", + ".py": "text/x-python", + ".js": "text/javascript", + ".ts": "text/typescript", + ".sh": "text/x-shellscript", + ".bash": "text/x-shellscript", + ".json": "application/json", + ".yaml": "text/yaml", + ".yml": "text/yaml", + ".xml": "application/xml", + ".html": "text/html", + ".css": "text/css", + ".sql": "text/x-sql", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".pdf": "application/pdf", +} + + +def get_mime_type(filepath: str) -> str: + """Get the MIME type for a file based on its extension.""" + _, ext = os.path.splitext(filepath) + return MIME_TYPES.get(ext.lower(), "application/octet-stream") + + +def generate_skills_xml(skill_map: dict[str, SkillMetadata]) -> str: + """Generate XML for injecting into system prompts. + + Format: + + + code-review + Perform structured code reviews... + skill://code-review + + + """ + lines: list[str] = [""] + + for skill in skill_map.values(): + lines.append(" ") + lines.append(f" {escape(skill.name)}") + lines.append(f" {escape(skill.description)}") + lines.append(f" skill://{escape(skill.name)}") + lines.append(" ") + + lines.append("") + return "\n".join(lines) diff --git a/examples/skills-as-resources/python/src/skills_as_resources/server.py b/examples/skills-as-resources/python/src/skills_as_resources/server.py new file mode 100644 index 0000000..541aa84 --- /dev/null +++ b/examples/skills-as-resources/python/src/skills_as_resources/server.py @@ -0,0 +1,209 @@ +""" +Skills as Resources — MCP Server (Python) + +A minimal reference implementation demonstrating the Resources approach +from the Skills Over MCP Interest Group: exposing agent skills via +MCP resources using the skill:// URI scheme. + +Exposes resources: + - skill://index — JSON index of all available skills + - skill://prompt-xml — XML for system prompt injection + - skill://{name} — Individual skill SKILL.md content + - skill://{name}/documents — List of supplementary files + - skill://{name}/document/{document_path} — Individual document (template) + +Note: The Python MCP SDK does not support RFC 6570 {+path} expansion, +so document paths containing "/" are URL-encoded (e.g., references%2FREFERENCE.md). +The SDK automatically URL-decodes them after template matching. + +Inspired by: +- skilljack-mcp by Ola Hungerford (https://github.com/olaservo/skilljack-mcp) +- skills-over-mcp by Keith Groves (https://github.com/keithagroves/skills-over-mcp) + +License: Apache-2.0 +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path +from urllib.parse import quote + +from mcp.server.fastmcp import FastMCP + +from .resource_helpers import generate_skills_xml +from .skill_discovery import discover_skills, load_document, load_skill_content + +# Resolve skills directory from CLI arg or default to ../sample-skills +if len(sys.argv) > 1: + skills_dir = str(Path(sys.argv[1]).resolve()) +else: + skills_dir = str( + Path(__file__).resolve().parent.parent.parent.parent / "sample-skills" + ) + +# Discover skills at startup +skill_map = discover_skills(skills_dir) +skill_names = list(skill_map.keys()) + +print( + f"[skills-as-resources] Discovered {len(skill_map)} skill(s): " + f"{', '.join(skill_names) or 'none'}", + file=sys.stderr, +) +for name, skill in skill_map.items(): + if skill.documents: + print( + f" - {name}: {len(skill.documents)} document(s)", + file=sys.stderr, + ) + +# Create MCP server +mcp = FastMCP( + name="skills-as-resources-example", +) + + +def _encode_document_path(path: str) -> str: + """URL-encode a document path for use in skill:// URIs. + + The Python MCP SDK uses [^/]+ regex for template parameters, + so forward slashes in paths must be percent-encoded. + """ + return quote(path, safe="") + + +def _build_index() -> list[dict]: + """Build the JSON index of all skills.""" + index = [] + for s in skill_map.values(): + entry: dict = { + "name": s.name, + "description": s.description, + "uri": f"skill://{s.name}", + "documentCount": len(s.documents), + } + if s.documents: + entry["documentsUri"] = f"skill://{s.name}/documents" + if s.metadata: + entry["metadata"] = s.metadata + index.append(entry) + return index + + +def _build_document_list(skill_name: str) -> dict: + """Build the document list for a skill.""" + skill = skill_map[skill_name] + return { + "skill": skill_name, + "documents": [ + { + "path": doc.path, + "mimeType": doc.mime_type, + "size": doc.size, + "uri": f"skill://{skill_name}/document/{_encode_document_path(doc.path)}", + } + for doc in skill.documents + ], + } + + +# --- Static resources --- + +@mcp.resource( + "skill://index", + name="skills-index", + description=( + "Index of all available skills with their descriptions, URIs, and document counts. " + f"Currently available: {', '.join(skill_names) or 'none'}" + ), + mime_type="application/json", +) +def get_index() -> str: + """Return JSON index of all available skills.""" + return json.dumps(_build_index(), indent=2) + + +@mcp.resource( + "skill://prompt-xml", + name="skills-prompt-xml", + description="XML representation of available skills for injecting into system prompts", + mime_type="application/xml", +) +def get_prompt_xml() -> str: + """Return XML representation for system prompt injection.""" + return generate_skills_xml(skill_map) + + +# Per-skill static resources registered in a loop. +# Uses closure binding to avoid Python's late-binding issue. +for _skill_name, _skill_meta in skill_map.items(): + + def _register_skill(s_name: str, s_meta): # noqa: ANN001 + @mcp.resource( + f"skill://{s_name}", + name=f"skill-{s_name}", + description=s_meta.description, + mime_type="text/markdown", + ) + def _get_skill() -> str: + try: + return load_skill_content(s_meta.path, skills_dir) + except (OSError, ValueError) as exc: + return f'# Error\n\nFailed to load skill "{s_name}": {exc}' + + if s_meta.documents: + @mcp.resource( + f"skill://{s_name}/documents", + name=f"skill-{s_name}-documents", + description=f"List of supplementary documents for the {s_name} skill", + mime_type="application/json", + ) + def _get_documents() -> str: + return json.dumps(_build_document_list(s_name), indent=2) + + _register_skill(_skill_name, _skill_meta) + + +# --- Dynamic resource template --- + +@mcp.resource( + "skill://{skill_name}/document/{document_path}", + name="skill-document", + description="Fetch a specific supplementary document from a skill", + mime_type="text/plain", +) +def get_document(skill_name: str, document_path: str) -> str: + """Fetch a supplementary document from a skill. + + The document_path is automatically URL-decoded by the SDK, + so encoded paths like "references%2FREFERENCE.md" arrive as + "references/REFERENCE.md". + """ + skill = skill_map.get(skill_name) + if not skill: + available = ", ".join(skill_names) or "none" + return f'# Error\n\nSkill "{skill_name}" not found. Available: {available}' + + doc = next((d for d in skill.documents if d.path == document_path), None) + if not doc: + available = "\n".join(f"- {d.path}" for d in skill.documents) + return ( + f'# Error\n\nDocument "{document_path}" not found in skill "{skill_name}".\n\n' + f"## Available Documents\n\n{available or 'No documents available.'}" + ) + + try: + return load_document(skill, document_path, skills_dir) + except (OSError, ValueError) as exc: + return f"# Error\n\nFailed to read document: {exc}" + + +def main() -> None: + """Entry point: run the MCP server via stdio transport.""" + mcp.run(transport="stdio") + + +if __name__ == "__main__": + main() diff --git a/examples/skills-as-resources/python/src/skills_as_resources/skill_discovery.py b/examples/skills-as-resources/python/src/skills_as_resources/skill_discovery.py new file mode 100644 index 0000000..8695202 --- /dev/null +++ b/examples/skills-as-resources/python/src/skills_as_resources/skill_discovery.py @@ -0,0 +1,328 @@ +""" +Skill discovery, content loading, and document scanning module. + +Discovers Agent Skills by scanning a directory for subdirectories +containing SKILL.md files, parses YAML frontmatter for metadata, +scans for supplementary documents, and provides secure content loading. + +Inspired by: +- skilljack-mcp by Ola Hungerford (https://github.com/olaservo/skilljack-mcp) +- skills-over-mcp by Keith Groves (https://github.com/keithagroves/skills-over-mcp) +""" + +from __future__ import annotations + +import logging +import os +from dataclasses import dataclass, field +from pathlib import Path + +import yaml + +logger = logging.getLogger(__name__) + +# Maximum file size for skill files (1MB) +MAX_FILE_SIZE = 1 * 1024 * 1024 + +# Map file extensions to MIME types +_MIME_TYPES: dict[str, str] = { + ".md": "text/markdown", + ".txt": "text/plain", + ".py": "text/x-python", + ".js": "text/javascript", + ".ts": "text/typescript", + ".sh": "text/x-shellscript", + ".bash": "text/x-shellscript", + ".json": "application/json", + ".yaml": "text/yaml", + ".yml": "text/yaml", + ".xml": "application/xml", + ".html": "text/html", + ".css": "text/css", + ".sql": "text/x-sql", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".pdf": "application/pdf", +} + + +@dataclass +class SkillDocument: + """A supplementary document found in a skill's subdirectories.""" + + path: str # Relative path from skill root (e.g., "references/REFERENCE.md") + mime_type: str + size: int + + +@dataclass +class SkillMetadata: + """Metadata extracted from a skill's SKILL.md YAML frontmatter.""" + + name: str + description: str + path: str # Absolute path to the SKILL.md file + skill_dir: str # Absolute path to the skill's directory + metadata: dict[str, str] = field(default_factory=dict) + documents: list[SkillDocument] = field(default_factory=list) + + +def _get_mime_type(filepath: str) -> str: + """Get the MIME type for a file based on its extension.""" + _, ext = os.path.splitext(filepath) + return _MIME_TYPES.get(ext.lower(), "application/octet-stream") + + +def _parse_frontmatter(content: str) -> tuple[dict, str]: + """Parse YAML frontmatter from SKILL.md content. + + Returns (frontmatter_dict, body_text). + """ + if not content.startswith("---"): + raise ValueError("SKILL.md must start with YAML frontmatter (---)") + + parts = content.split("---") + if len(parts) < 3: + raise ValueError("SKILL.md frontmatter not properly closed with ---") + + # Use safe_load to prevent arbitrary code execution + frontmatter = yaml.safe_load(parts[1]) + if not isinstance(frontmatter, dict): + raise ValueError("SKILL.md frontmatter must be a YAML mapping") + + body = "---".join(parts[2:]).strip() + return frontmatter, body + + +def _is_path_within_base(target: Path, base: Path) -> bool: + """Check if a resolved path is within the allowed base directory.""" + try: + resolved_base = base.resolve(strict=True) + resolved_target = target.resolve(strict=True) + return resolved_target == resolved_base or str( + resolved_target + ).startswith(str(resolved_base) + os.sep) + except OSError: + # Fall back to non-strict resolve + resolved_base = base.resolve() + resolved_target = target.resolve() + return str(resolved_target).startswith(str(resolved_base) + os.sep) + + +def _scan_dir(dir_path: Path, relative_to: Path, base_dir: Path) -> list[SkillDocument]: + """Recursively scan a directory for files, returning SkillDocument entries.""" + documents: list[SkillDocument] = [] + + if not dir_path.is_dir(): + return documents + + try: + entries = list(dir_path.iterdir()) + except OSError: + return documents + + for entry in entries: + # Security: verify path stays within the skills directory + if not _is_path_within_base(entry, base_dir): + continue + + if entry.is_file(): + try: + stat = entry.stat() + if stat.st_size > MAX_FILE_SIZE: + continue + + relative_path = str(entry.relative_to(relative_to)).replace("\\", "/") + documents.append( + SkillDocument( + path=relative_path, + mime_type=_get_mime_type(entry.name), + size=stat.st_size, + ) + ) + except OSError: + pass + elif entry.is_dir(): + documents.extend(_scan_dir(entry, relative_to, base_dir)) + + return documents + + +def scan_documents(skill_dir: str, base_dir: str) -> list[SkillDocument]: + """Scan a skill directory for supplementary documents. + + Finds all files in subdirectories of the skill directory, + excluding SKILL.md itself. + """ + documents: list[SkillDocument] = [] + skill_path = Path(skill_dir) + base_path = Path(base_dir) + + try: + entries = list(skill_path.iterdir()) + except OSError: + return documents + + for entry in entries: + if entry.is_dir(): + documents.extend(_scan_dir(entry, skill_path, base_path)) + + return documents + + +def discover_skills(skills_dir: str) -> dict[str, SkillMetadata]: + """Discover all skills in a directory. + + Scans for immediate subdirectories containing SKILL.md files, + and scans for supplementary documents in each skill directory. + Security: skips files larger than MAX_FILE_SIZE, validates frontmatter. + """ + skill_map: dict[str, SkillMetadata] = {} + resolved_dir = Path(skills_dir).resolve() + + if not resolved_dir.is_dir(): + logger.error("Skills directory not found: %s", resolved_dir) + return skill_map + + for entry in resolved_dir.iterdir(): + if not entry.is_dir(): + continue + + # Find SKILL.md (prefer uppercase, accept lowercase) + skill_md_path = None + for name in ("SKILL.md", "skill.md"): + candidate = entry / name + if candidate.exists(): + skill_md_path = candidate + break + + if skill_md_path is None: + continue + + # Security: check file size before reading + stat = skill_md_path.stat() + if stat.st_size > MAX_FILE_SIZE: + logger.error( + "Skipping %s: file size %.2fMB exceeds limit", + skill_md_path, + stat.st_size / 1024 / 1024, + ) + continue + + # Security: verify path is within skills directory + if not _is_path_within_base(skill_md_path, resolved_dir): + logger.error( + "Skipping %s: path escapes skills directory", skill_md_path + ) + continue + + try: + content = skill_md_path.read_text(encoding="utf-8") + frontmatter, _body = _parse_frontmatter(content) + + name = frontmatter.get("name") + description = frontmatter.get("description") + + if not isinstance(name, str) or not name.strip(): + logger.error( + "Skill at %s: missing or invalid 'name' field", entry + ) + continue + if not isinstance(description, str) or not description.strip(): + logger.error( + "Skill at %s: missing or invalid 'description' field", entry + ) + continue + + # Extract optional metadata + extra_metadata: dict[str, str] = {} + raw_meta = frontmatter.get("metadata") + if isinstance(raw_meta, dict): + for k, v in raw_meta.items(): + if isinstance(v, str): + extra_metadata[k] = v + + skill_name = name.strip() + if skill_name in skill_map: + logger.warning( + "Duplicate skill name '%s' at %s — keeping first", + skill_name, + skill_md_path, + ) + continue + + # Scan for supplementary documents + skill_dir_str = str(entry) + documents = scan_documents(skill_dir_str, str(resolved_dir)) + + skill_map[skill_name] = SkillMetadata( + name=skill_name, + description=description.strip(), + path=str(skill_md_path), + skill_dir=skill_dir_str, + metadata=extra_metadata if extra_metadata else {}, + documents=documents, + ) + except (OSError, ValueError) as exc: + logger.error("Failed to parse skill at %s: %s", entry, exc) + + return skill_map + + +def load_skill_content(skill_path: str, skills_dir: str) -> str: + """Load the full content of a SKILL.md file. + + Security: validates path is within skills directory, only reads .md files, + and enforces a file size limit. + """ + target = Path(skill_path) + base = Path(skills_dir) + + # Security: only allow .md files + if target.suffix.lower() != ".md": + raise ValueError("Only .md files can be read") + + # Security: verify path is within skills directory + if not _is_path_within_base(target, base): + raise ValueError("Path escapes the skills directory") + + # Security: check file size + stat = target.stat() + if stat.st_size > MAX_FILE_SIZE: + raise ValueError( + f"File size {stat.st_size / 1024 / 1024:.2f}MB exceeds " + f"{MAX_FILE_SIZE / 1024 / 1024:.0f}MB limit" + ) + + return target.read_text(encoding="utf-8") + + +def load_document(skill: SkillMetadata, document_path: str, skills_dir: str) -> str: + """Load a supplementary document from a skill directory. + + Security: validates path is within skills directory, rejects path + traversal attempts, and enforces a file size limit. + """ + # Security: reject path traversal attempts + if ".." in document_path: + raise ValueError("Path traversal not allowed") + + target = Path(skill.skill_dir) / document_path + base = Path(skills_dir) + + # Security: verify path is within skills directory + if not _is_path_within_base(target, base): + raise ValueError("Path escapes the skills directory") + + # Security: check file size + stat = target.stat() + if stat.st_size > MAX_FILE_SIZE: + raise ValueError( + f"File size {stat.st_size / 1024 / 1024:.2f}MB exceeds " + f"{MAX_FILE_SIZE / 1024 / 1024:.0f}MB limit" + ) + + return target.read_text(encoding="utf-8") diff --git a/examples/skills-as-resources/sample-skills/code-review/SKILL.md b/examples/skills-as-resources/sample-skills/code-review/SKILL.md new file mode 100644 index 0000000..123dc3a --- /dev/null +++ b/examples/skills-as-resources/sample-skills/code-review/SKILL.md @@ -0,0 +1,47 @@ +--- +name: code-review +description: Perform structured code reviews focusing on correctness, readability, and maintainability. Use when asked to review code changes or pull requests. +metadata: + author: skills-over-mcp-ig + version: "0.1" +--- + +# Code Review + +Perform structured code reviews using a consistent methodology. + +## When to Use + +- User asks you to review code, a diff, or a pull request +- User asks for feedback on code quality +- You are evaluating code changes before merge + +## Process + +1. **Understand the context** — read the PR description or ask what the change is trying to accomplish +2. **Review for correctness** — does the code do what it claims? Are there logic errors, off-by-one bugs, or unhandled edge cases? +3. **Review for security** — check for injection vulnerabilities, improper input validation, hardcoded secrets, and OWASP top 10 issues +4. **Review for readability** — are names clear? Is the structure easy to follow? Is there unnecessary complexity? +5. **Review for maintainability** — is the code testable? Are dependencies reasonable? Will this be easy to change later? +6. **Check the tests** — are there tests? Do they cover the important cases? Are they testing behavior, not implementation? + +## Severity Levels + +- **Blocker**: Must fix before merge (security issues, data loss risk, broken functionality) +- **Major**: Should fix before merge (logic errors, missing edge cases, poor error handling) +- **Minor**: Nice to fix (naming, style, minor simplifications) +- **Nit**: Optional (personal preference, cosmetic) + +## Reference + +For a detailed checklist, see `references/REFERENCE.md` in this skill's directory. + +## Output Format + +For each finding: +- **File and line**: Where the issue is +- **Severity**: Blocker / Major / Minor / Nit +- **Issue**: What's wrong +- **Suggestion**: How to fix it + +End with an overall summary: approve, request changes, or comment. diff --git a/examples/skills-as-resources/sample-skills/code-review/references/REFERENCE.md b/examples/skills-as-resources/sample-skills/code-review/references/REFERENCE.md new file mode 100644 index 0000000..718b739 --- /dev/null +++ b/examples/skills-as-resources/sample-skills/code-review/references/REFERENCE.md @@ -0,0 +1,36 @@ +# Code Review Checklist + +## Correctness +- [ ] Logic matches the stated intent +- [ ] Edge cases handled (null, empty, boundary values) +- [ ] Error paths return meaningful messages +- [ ] Async operations properly awaited +- [ ] Resources cleaned up (connections, file handles, timers) + +## Security +- [ ] User input validated and sanitized +- [ ] No SQL injection, XSS, or command injection vectors +- [ ] No hardcoded secrets or credentials +- [ ] Authentication/authorization checks in place +- [ ] Sensitive data not logged or exposed in errors + +## Readability +- [ ] Names describe purpose (not implementation) +- [ ] Functions do one thing +- [ ] No deeply nested conditionals (max 3 levels) +- [ ] Comments explain "why", not "what" +- [ ] Consistent formatting with project style + +## Maintainability +- [ ] No code duplication (DRY where appropriate) +- [ ] Dependencies are justified +- [ ] Configuration externalized (not hardcoded) +- [ ] Backward compatibility considered +- [ ] Migration path documented if breaking + +## Testing +- [ ] Tests exist for new/changed behavior +- [ ] Tests cover happy path and error cases +- [ ] Tests are independent (no shared mutable state) +- [ ] Test names describe the scenario +- [ ] No flaky tests (timing, ordering, external dependencies) diff --git a/examples/skills-as-resources/sample-skills/git-commit-review/SKILL.md b/examples/skills-as-resources/sample-skills/git-commit-review/SKILL.md new file mode 100644 index 0000000..c2dc72d --- /dev/null +++ b/examples/skills-as-resources/sample-skills/git-commit-review/SKILL.md @@ -0,0 +1,42 @@ +--- +name: git-commit-review +description: Review git commits for quality, conventional commit format compliance, and potential issues. Use when asked to review commits or improve commit messages. +metadata: + author: skills-over-mcp-ig + version: "0.1" +--- + +# Git Commit Review + +Review git commits against conventional commit standards and common quality issues. + +## When to Use + +- User asks you to review a commit or commit message +- User asks for help improving commit quality +- You are reviewing a PR and want to assess commit hygiene + +## Process + +1. **Read the commit message** — check for conventional commit format: `type(scope): description` +2. **Verify the type** — must be one of: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `ci`, `build`, `perf` +3. **Check the description** — should be imperative mood, lowercase, no period at end, under 72 characters +4. **Review the body** (if present) — should explain *why* not *what*, wrapped at 72 characters +5. **Check for breaking changes** — must include `BREAKING CHANGE:` footer or `!` after type/scope +6. **Assess the diff** — does the commit message accurately describe the changes? + +## Common Issues + +- Vague messages ("fix stuff", "update code", "wip") +- Type mismatch (using `feat` for a bug fix) +- Scope too broad (single commit touching unrelated files) +- Missing breaking change annotation +- Commit contains unrelated changes that should be separate commits + +## Output Format + +Provide a structured review: +- **Format**: Pass/Fail with specific issues +- **Message quality**: Rating and suggestions +- **Scope assessment**: Whether changes match the stated scope +- **Recommendations**: Concrete improvements diff --git a/examples/skills-as-resources/typescript/package-lock.json b/examples/skills-as-resources/typescript/package-lock.json new file mode 100644 index 0000000..2f11610 --- /dev/null +++ b/examples/skills-as-resources/typescript/package-lock.json @@ -0,0 +1,1736 @@ +{ + "name": "@skills-over-mcp-ig/skills-as-resources-example", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@skills-over-mcp-ig/skills-as-resources-example", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.0", + "yaml": "^2.7.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/node": { + "version": "22.19.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.10.tgz", + "integrity": "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.8.tgz", + "integrity": "sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/examples/skills-as-resources/typescript/package.json b/examples/skills-as-resources/typescript/package.json new file mode 100644 index 0000000..381336d --- /dev/null +++ b/examples/skills-as-resources/typescript/package.json @@ -0,0 +1,25 @@ +{ + "name": "@skills-over-mcp-ig/skills-as-resources-example", + "version": "0.1.0", + "description": "Minimal reference implementation: Skills as MCP Resources", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsx src/index.ts" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.0", + "yaml": "^2.7.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "license": "Apache-2.0" +} diff --git a/examples/skills-as-resources/typescript/src/index.ts b/examples/skills-as-resources/typescript/src/index.ts new file mode 100644 index 0000000..16a5c29 --- /dev/null +++ b/examples/skills-as-resources/typescript/src/index.ts @@ -0,0 +1,278 @@ +#!/usr/bin/env node +/** + * Skills as Resources — MCP Server (TypeScript) + * + * A minimal reference implementation demonstrating the Resources approach + * from the Skills Over MCP Interest Group: exposing agent skills via + * MCP resources using the skill:// URI scheme. + * + * Exposes resources: + * - skill://index — JSON index of all available skills + * - skill://prompt-xml — XML for system prompt injection + * - skill://{name} — Individual skill SKILL.md content + * - skill://{name}/documents — List of supplementary files + * - skill://{name}/document/{+documentPath} — Individual document (template) + * + * Inspired by: + * - skilljack-mcp by Ola Hungerford (https://github.com/olaservo/skilljack-mcp) + * - skills-over-mcp by Keith Groves (https://github.com/keithagroves/skills-over-mcp) + * + * @license Apache-2.0 + */ + +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { discoverSkills, loadSkillContent, loadDocument } from "./skill-discovery.js"; +import { generateSkillsXML } from "./resource-helpers.js"; +import type { SkillSummary } from "./types.js"; + +// Resolve skills directory from CLI arg or default to ../sample-skills +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const skillsDir = process.argv[2] + ? path.resolve(process.argv[2]) + : path.resolve(__dirname, "../../sample-skills"); + +// Discover skills at startup +const skillMap = discoverSkills(skillsDir); +const skillNames = Array.from(skillMap.keys()); + +console.error( + `[skills-as-resources] Discovered ${skillMap.size} skill(s): ${skillNames.join(", ") || "none"}` +); +for (const [name, skill] of skillMap) { + if (skill.documents.length > 0) { + console.error( + ` - ${name}: ${skill.documents.length} document(s)` + ); + } +} + +// Create MCP server with resources.listChanged capability +const server = new McpServer( + { name: "skills-as-resources-example", version: "0.1.0" }, + { capabilities: { resources: { listChanged: true } } } +); + +// --- Static resources --- + +// Resource: skill://index — JSON index of all available skills +server.registerResource( + "skills-index", + "skill://index", + { + description: + "Index of all available skills with their descriptions, URIs, and document counts. " + + `Currently available: ${skillNames.join(", ") || "none"}`, + mimeType: "application/json", + }, + async (uri) => { + const index: SkillSummary[] = Array.from(skillMap.values()).map((s) => ({ + name: s.name, + description: s.description, + uri: `skill://${s.name}`, + ...(s.documents.length > 0 && { + documentsUri: `skill://${s.name}/documents`, + }), + documentCount: s.documents.length, + ...(s.metadata && { metadata: s.metadata }), + })); + + return { + contents: [ + { + uri: uri.href, + text: JSON.stringify(index, null, 2), + }, + ], + }; + } +); + +// Resource: skill://prompt-xml — XML for system prompt injection +server.registerResource( + "skills-prompt-xml", + "skill://prompt-xml", + { + description: + "XML representation of available skills for injecting into system prompts", + mimeType: "application/xml", + }, + async (uri) => ({ + contents: [ + { + uri: uri.href, + text: generateSkillsXML(skillMap), + }, + ], + }) +); + +// Per-skill static resources +for (const [name, skill] of skillMap) { + // Resource: skill://{name} — individual skill SKILL.md content + server.registerResource( + `skill-${name}`, + `skill://${name}`, + { + description: skill.description, + mimeType: "text/markdown", + }, + async (uri) => { + try { + const content = loadSkillContent(skill.path, skillsDir); + return { + contents: [{ uri: uri.href, text: content }], + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + contents: [ + { + uri: uri.href, + text: `# Error\n\nFailed to load skill "${name}": ${message}`, + }, + ], + }; + } + } + ); + + // Resource: skill://{name}/documents — list of supplementary files + if (skill.documents.length > 0) { + server.registerResource( + `skill-${name}-documents`, + `skill://${name}/documents`, + { + description: `List of supplementary documents for the ${name} skill`, + mimeType: "application/json", + }, + async (uri) => ({ + contents: [ + { + uri: uri.href, + text: JSON.stringify( + { + skill: name, + documents: skill.documents.map((doc) => ({ + path: doc.path, + mimeType: doc.mimeType, + size: doc.size, + uri: `skill://${name}/document/${doc.path}`, + })), + }, + null, + 2 + ), + }, + ], + }) + ); + } +} + +// --- Dynamic resource template --- + +// Template: skill://{skillName}/document/{+documentPath} +// The {+} prefix uses RFC 6570 reserved expansion, matching paths with slashes +server.registerResource( + "skill-document", + new ResourceTemplate("skill://{skillName}/document/{+documentPath}", { + list: async () => { + const resources = Array.from(skillMap.values()).flatMap((skill) => + skill.documents.map((doc) => ({ + uri: `skill://${skill.name}/document/${doc.path}`, + name: `${skill.name}/${doc.path}`, + description: `Document from ${skill.name} skill`, + mimeType: doc.mimeType, + })) + ); + return { resources }; + }, + complete: { + skillName: (value) => { + return Array.from(skillMap.values()) + .filter((s) => s.documents.length > 0) + .map((s) => s.name) + .filter((name) => name.startsWith(value)); + }, + documentPath: (value, context) => { + const skillName = context?.arguments?.skillName; + if (!skillName) return []; + + const skill = skillMap.get(skillName); + if (!skill) return []; + + return skill.documents + .map((d) => d.path) + .filter((p) => p.startsWith(value)); + }, + }, + }), + { + description: "Fetch a specific supplementary document from a skill", + mimeType: "text/plain", + }, + async (uri, variables) => { + const skillName = Array.isArray(variables.skillName) + ? variables.skillName[0] + : variables.skillName; + const documentPath = Array.isArray(variables.documentPath) + ? variables.documentPath[0] + : variables.documentPath; + + const skill = skillMap.get(skillName); + if (!skill) { + return { + contents: [ + { + uri: uri.href, + text: `# Error\n\nSkill "${skillName}" not found. Available: ${skillNames.join(", ") || "none"}`, + }, + ], + }; + } + + const doc = skill.documents.find((d) => d.path === documentPath); + if (!doc) { + const available = skill.documents.map((d) => `- ${d.path}`).join("\n"); + return { + contents: [ + { + uri: uri.href, + text: `# Error\n\nDocument "${documentPath}" not found in skill "${skillName}".\n\n## Available Documents\n\n${available || "No documents available."}`, + }, + ], + }; + } + + try { + const content = loadDocument(skill, documentPath, skillsDir); + return { + contents: [ + { + uri: uri.href, + text: content, + mimeType: doc.mimeType, + }, + ], + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return { + contents: [ + { + uri: uri.href, + text: `# Error\n\nFailed to read document: ${message}`, + }, + ], + }; + } + } +); + +// Connect via stdio transport +const transport = new StdioServerTransport(); +await server.connect(transport); +console.error("[skills-as-resources] Server connected via stdio"); diff --git a/examples/skills-as-resources/typescript/src/resource-helpers.ts b/examples/skills-as-resources/typescript/src/resource-helpers.ts new file mode 100644 index 0000000..20e8fe0 --- /dev/null +++ b/examples/skills-as-resources/typescript/src/resource-helpers.ts @@ -0,0 +1,87 @@ +/** + * Resource helper utilities for the Skills as Resources implementation. + * + * Provides XML generation for system prompt injection and MIME type mapping + * for skill documents. + * + * Inspired by: + * - skills-over-mcp by Keith Groves (https://github.com/keithagroves/skills-over-mcp) + */ + +import * as path from "node:path"; +import type { SkillMetadata } from "./types.js"; + +/** Map file extensions to MIME types. */ +const MIME_TYPES: Record = { + ".md": "text/markdown", + ".txt": "text/plain", + ".py": "text/x-python", + ".js": "text/javascript", + ".ts": "text/typescript", + ".sh": "text/x-shellscript", + ".bash": "text/x-shellscript", + ".json": "application/json", + ".yaml": "text/yaml", + ".yml": "text/yaml", + ".xml": "application/xml", + ".html": "text/html", + ".css": "text/css", + ".sql": "text/x-sql", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".svg": "image/svg+xml", + ".pdf": "application/pdf", +}; + +/** + * Get the MIME type for a file based on its extension. + */ +export function getMimeType(filepath: string): string { + const ext = path.extname(filepath).toLowerCase(); + return MIME_TYPES[ext] || "application/octet-stream"; +} + +/** + * Escape XML special characters. + */ +function escapeXml(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * Generate XML for injecting into system prompts. + * + * Format: + * ```xml + * + * + * code-review + * Perform structured code reviews... + * skill://code-review + * + * + * ``` + */ +export function generateSkillsXML( + skillMap: Map +): string { + const lines: string[] = [""]; + + for (const skill of skillMap.values()) { + lines.push(" "); + lines.push(` ${escapeXml(skill.name)}`); + lines.push(` ${escapeXml(skill.description)}`); + lines.push(` skill://${escapeXml(skill.name)}`); + lines.push(" "); + } + + lines.push(""); + return lines.join("\n"); +} diff --git a/examples/skills-as-resources/typescript/src/skill-discovery.ts b/examples/skills-as-resources/typescript/src/skill-discovery.ts new file mode 100644 index 0000000..a00384a --- /dev/null +++ b/examples/skills-as-resources/typescript/src/skill-discovery.ts @@ -0,0 +1,318 @@ +/** + * Skill discovery, content loading, and document scanning module. + * + * Discovers Agent Skills by scanning a directory for subdirectories + * containing SKILL.md files, parses YAML frontmatter for metadata, + * scans for supplementary documents, and provides secure content loading. + * + * Inspired by: + * - skilljack-mcp by Ola Hungerford (https://github.com/olaservo/skilljack-mcp) + * - skills-over-mcp by Keith Groves (https://github.com/keithagroves/skills-over-mcp) + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import { parse as parseYaml } from "yaml"; +import type { SkillMetadata, SkillDocument } from "./types.js"; +import { getMimeType } from "./resource-helpers.js"; + +/** Maximum file size for skill files (1MB). */ +const MAX_FILE_SIZE = 1 * 1024 * 1024; + +/** + * Parse YAML frontmatter from SKILL.md content. + * Expects content to start with --- and have a closing ---. + */ +function parseFrontmatter(content: string): { + frontmatter: Record; + body: string; +} { + if (!content.startsWith("---")) { + throw new Error("SKILL.md must start with YAML frontmatter (---)"); + } + + const parts = content.split("---"); + if (parts.length < 3) { + throw new Error("SKILL.md frontmatter not properly closed with ---"); + } + + const frontmatter = parseYaml(parts[1]) as Record; + if (typeof frontmatter !== "object" || frontmatter === null) { + throw new Error("SKILL.md frontmatter must be a YAML mapping"); + } + + const body = parts.slice(2).join("---").trim(); + return { frontmatter, body }; +} + +/** + * Check if a resolved path is within the allowed base directory. + * Uses fs.realpathSync to resolve symlinks and prevent escape attacks. + */ +export function isPathWithinBase( + targetPath: string, + baseDir: string +): boolean { + try { + const realBase = fs.realpathSync(baseDir); + const realTarget = fs.realpathSync(targetPath); + const normalizedBase = realBase + path.sep; + return realTarget === realBase || realTarget.startsWith(normalizedBase); + } catch { + // Fall back to resolve check if realpathSync fails + const normalizedBase = path.resolve(baseDir) + path.sep; + const normalizedPath = path.resolve(targetPath); + return normalizedPath.startsWith(normalizedBase); + } +} + +/** + * Recursively scan a directory for files, returning SkillDocument entries. + * Security: applies path traversal checks and file size limits. + */ +function scanDir( + dirPath: string, + relativeTo: string, + baseDir: string +): SkillDocument[] { + const documents: SkillDocument[] = []; + + if (!fs.existsSync(dirPath)) return documents; + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dirPath, { withFileTypes: true }); + } catch { + return documents; + } + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + // Security: verify path stays within the skills directory + if (!isPathWithinBase(fullPath, baseDir)) continue; + + if (entry.isFile()) { + try { + const stat = fs.statSync(fullPath); + if (stat.size > MAX_FILE_SIZE) continue; + + const relativePath = path.relative(relativeTo, fullPath).replace(/\\/g, "/"); + documents.push({ + path: relativePath, + mimeType: getMimeType(entry.name), + size: stat.size, + }); + } catch { + // Skip files we can't stat + } + } else if (entry.isDirectory()) { + // Recurse into subdirectories + documents.push(...scanDir(fullPath, relativeTo, baseDir)); + } + } + + return documents; +} + +/** + * Scan a skill directory for supplementary documents. + * Finds all files in subdirectories of the skill directory, + * excluding SKILL.md itself. + */ +export function scanDocuments( + skillDir: string, + baseDir: string +): SkillDocument[] { + const documents: SkillDocument[] = []; + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(skillDir, { withFileTypes: true }); + } catch { + return documents; + } + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const subDirPath = path.join(skillDir, entry.name); + documents.push(...scanDir(subDirPath, skillDir, baseDir)); + } + + return documents; +} + +/** + * Discover all skills in a directory. + * Scans for immediate subdirectories containing SKILL.md (or skill.md) files, + * and scans for supplementary documents in each skill directory. + * + * Security: Skips files larger than MAX_FILE_SIZE, validates frontmatter fields. + */ +export function discoverSkills(skillsDir: string): Map { + const skillMap = new Map(); + const resolvedDir = path.resolve(skillsDir); + + if (!fs.existsSync(resolvedDir)) { + console.error(`Skills directory not found: ${resolvedDir}`); + return skillMap; + } + + const entries = fs.readdirSync(resolvedDir, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + const skillDir = path.join(resolvedDir, entry.name); + + // Find SKILL.md (prefer uppercase, accept lowercase) + let skillMdPath: string | null = null; + for (const name of ["SKILL.md", "skill.md"]) { + const candidate = path.join(skillDir, name); + if (fs.existsSync(candidate)) { + skillMdPath = candidate; + break; + } + } + + if (!skillMdPath) continue; + + // Security: check file size before reading + const stat = fs.statSync(skillMdPath); + if (stat.size > MAX_FILE_SIZE) { + console.error( + `Skipping ${skillMdPath}: file size ${(stat.size / 1024 / 1024).toFixed(2)}MB exceeds limit` + ); + continue; + } + + // Security: verify path is within skills directory + if (!isPathWithinBase(skillMdPath, resolvedDir)) { + console.error(`Skipping ${skillMdPath}: path escapes skills directory`); + continue; + } + + try { + const content = fs.readFileSync(skillMdPath, "utf-8"); + const { frontmatter } = parseFrontmatter(content); + + const name = frontmatter.name; + const description = frontmatter.description; + + if (typeof name !== "string" || !name.trim()) { + console.error(`Skill at ${skillDir}: missing or invalid 'name' field`); + continue; + } + if (typeof description !== "string" || !description.trim()) { + console.error( + `Skill at ${skillDir}: missing or invalid 'description' field` + ); + continue; + } + + // Extract optional metadata fields + const metadata: Record = {}; + if ( + frontmatter.metadata && + typeof frontmatter.metadata === "object" + ) { + for (const [k, v] of Object.entries( + frontmatter.metadata as Record + )) { + if (typeof v === "string") { + metadata[k] = v; + } + } + } + + const trimmedName = name.trim(); + if (skillMap.has(trimmedName)) { + console.error( + `Warning: Duplicate skill name "${trimmedName}" at ${skillMdPath} — keeping first` + ); + continue; + } + + // Scan for supplementary documents + const documents = scanDocuments(skillDir, resolvedDir); + + skillMap.set(trimmedName, { + name: trimmedName, + description: description.trim(), + path: skillMdPath, + skillDir, + metadata: Object.keys(metadata).length > 0 ? metadata : undefined, + documents, + }); + } catch (error) { + console.error(`Failed to parse skill at ${skillDir}:`, error); + } + } + + return skillMap; +} + +/** + * Load the full content of a SKILL.md file. + * + * Security: Validates that the path is within the skills directory, + * only reads .md files, and enforces a file size limit. + */ +export function loadSkillContent( + skillPath: string, + skillsDir: string +): string { + // Security: only allow .md files + if (!skillPath.endsWith(".md")) { + throw new Error("Only .md files can be read"); + } + + // Security: verify path is within skills directory + if (!isPathWithinBase(skillPath, skillsDir)) { + throw new Error("Path escapes the skills directory"); + } + + // Security: check file size + const stat = fs.statSync(skillPath); + if (stat.size > MAX_FILE_SIZE) { + throw new Error( + `File size ${(stat.size / 1024 / 1024).toFixed(2)}MB exceeds ${(MAX_FILE_SIZE / 1024 / 1024).toFixed(0)}MB limit` + ); + } + + return fs.readFileSync(skillPath, "utf-8"); +} + +/** + * Load a supplementary document from a skill directory. + * + * Security: Validates that the path is within the skills directory, + * rejects path traversal attempts, and enforces a file size limit. + */ +export function loadDocument( + skill: SkillMetadata, + documentPath: string, + skillsDir: string +): string { + // Security: reject path traversal attempts + if (documentPath.includes("..")) { + throw new Error("Path traversal not allowed"); + } + + const fullPath = path.join(skill.skillDir, documentPath); + + // Security: verify path is within skills directory + if (!isPathWithinBase(fullPath, skillsDir)) { + throw new Error("Path escapes the skills directory"); + } + + // Security: check file size + const stat = fs.statSync(fullPath); + if (stat.size > MAX_FILE_SIZE) { + throw new Error( + `File size ${(stat.size / 1024 / 1024).toFixed(2)}MB exceeds ${(MAX_FILE_SIZE / 1024 / 1024).toFixed(0)}MB limit` + ); + } + + return fs.readFileSync(fullPath, "utf-8"); +} diff --git a/examples/skills-as-resources/typescript/src/types.ts b/examples/skills-as-resources/typescript/src/types.ts new file mode 100644 index 0000000..604728b --- /dev/null +++ b/examples/skills-as-resources/typescript/src/types.ts @@ -0,0 +1,44 @@ +/** + * Type definitions for the Skills as Resources reference implementation. + * + * Inspired by: + * - skilljack-mcp by Ola Hungerford (https://github.com/olaservo/skilljack-mcp) + * - skills-over-mcp by Keith Groves (https://github.com/keithagroves/skills-over-mcp) + */ + +/** + * A supplementary document found in a skill's subdirectories. + */ +export interface SkillDocument { + /** Relative path from skill root (e.g., "references/REFERENCE.md") */ + path: string; + /** MIME type based on file extension */ + mimeType: string; + /** File size in bytes */ + size: number; +} + +/** + * Metadata extracted from a skill's SKILL.md YAML frontmatter, + * extended with document scanning results. + */ +export interface SkillMetadata { + name: string; + description: string; + path: string; // Absolute path to the SKILL.md file + skillDir: string; // Absolute path to the skill's directory + metadata?: Record; // Optional extra frontmatter fields + documents: SkillDocument[]; // Supplementary files found in subdirectories +} + +/** + * Summary returned in the skill://index resource (progressive disclosure). + */ +export interface SkillSummary { + name: string; + description: string; + uri: string; // skill://{name} + documentsUri?: string; // skill://{name}/documents (only if documents exist) + documentCount: number; + metadata?: Record; +} diff --git a/examples/skills-as-resources/typescript/tsconfig.json b/examples/skills-as-resources/typescript/tsconfig.json new file mode 100644 index 0000000..486fed8 --- /dev/null +++ b/examples/skills-as-resources/typescript/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true + }, + "include": ["src/**/*"] +}