diff --git a/.gitignore b/.gitignore index d6b130c..401277c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .claude/settings.local.json +node_modules/ +dist/ diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..c62aa9a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,76 @@ +# Skills Extension SEP — Reference Examples + +End-to-end TypeScript reference for [SEP-2640](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640) (Skills Extension), exercising every normative surface in the SEP against the bundled SDK. + +## Layout + +``` +examples/ +├── sample-skills/ ← individually-served skills (4 skill-md entries) +│ ├── code-review/ +│ │ ├── SKILL.md +│ │ └── references/REFERENCE.md +│ ├── git-commit-review/SKILL.md +│ └── acme/ +│ ├── onboarding/SKILL.md +│ └── billing/refunds/ +│ ├── SKILL.md +│ └── templates/refund-email-template.md +│ +├── sample-archive-source/ ← packed at server startup → archive entry +│ └── pdf-processing/ +│ ├── SKILL.md +│ └── references/FORMS.md +│ +├── skills-server/typescript/ ← MCP server using @modelcontextprotocol/experimental-ext-skills/server +│ ├── package.json +│ ├── tsconfig.json +│ └── src/ +│ ├── index.ts ← discovers, registers, declares capability +│ └── pack-archive.ts ← in-memory tar.gz packer (example-only) +│ +└── skills-client/typescript/ ← MCP client using @modelcontextprotocol/experimental-ext-skills/client + ├── package.json + ├── tsconfig.json + └── src/index.ts ← walks every client-side surface +``` + +## What it demonstrates + +| SEP feature | Server | Client | +|---|---|---| +| Capability declaration `io.modelcontextprotocol/skills` (`directoryRead: true`) | `declareSkillsExtension(server, { directoryRead: true })` | observed in `initialize` response | +| `skill://` scheme + multi-segment paths | 4 file skills, 1 multi-segment (`acme/billing/refunds`) | parses each URI | +| `skill://index.json` enumeration (verbatim frontmatter, `url`+`digest`, `archives`) | generated by `registerSkillResources()` | `listSkillsFromIndex()` | +| File-skill entries (`url` + `digest`) | each discovered SKILL.md | resolved as `uri`, digest carried on the summary | +| Archive entries (`.tar.gz`, in `archives[]`) | `pdf-processing` packed at startup | `readSkillArchive()` fetches + unpacks | +| `resources/directory/read` (`inode/directory`) | handler installed via `directoryRead: true` | `readDirectory()` / `walkDirectory()` | +| Digest verification | `sha256:` digests emitted in the index | `verifyDigest()` against the read | +| Supporting-file flow | catch-all template `skill://{+skillFilePath}` | `readSkillDocument()` | +| `read_resource` tool schema | n/a (host concern) | `READ_RESOURCE_TOOL` exported | + +## Run it + +```bash +# Build the SDK first (workspace dep) +cd typescript/sdk +npm install +npm run build + +# Build the example server +cd ../../examples/skills-server/typescript +npm install +npm run build + +# Build and run the example client (it spawns the server via stdio) +cd ../../skills-client/typescript +npm install +npm run build +npm start +``` + +The client output walks through its sections, each demonstrating one SEP surface — index discovery, `resources/directory/read` enumeration, fallback enumeration, skill-md reads, archive fetch+unpack, digest verification, and the supporting-file flow. + +## Archive safety + +The pdf-processing archive demo exercises the SEP's archive-safety requirements end-to-end: the SDK rejects path-traversal segments, absolute paths, drive-letter prefixes, and out-of-tree symlinks at unpack time, with bounded total size, per-file size, and entry count to defend against decompression bombs. See [`typescript/sdk/src/archive.test.ts`](../typescript/sdk/src/archive.test.ts) for the full safety suite. diff --git a/examples/sample-archive-source/pdf-processing/SKILL.md b/examples/sample-archive-source/pdf-processing/SKILL.md new file mode 100644 index 0000000..16573bd --- /dev/null +++ b/examples/sample-archive-source/pdf-processing/SKILL.md @@ -0,0 +1,38 @@ +--- +name: pdf-processing +description: Extract text and form data from PDFs, fill PDF forms, and merge multi-page documents. Use whenever the user asks to read, fill, or assemble PDFs. +--- + +# PDF Processing + +Workflow for extracting structured data from PDFs and assembling new ones from +templates and form input. + +This skill is distributed as an **archive** (`skill://pdf-processing.tar.gz`) +to demonstrate SEP-2640 archive distribution. The host fetches the archive, +unpacks it with archive-safety checks, and presents files at +`skill://pdf-processing/` exactly as if they were served as +individual MCP resources. + +## When to use + +- **Extract text** — pull plain text from a PDF, preserving page boundaries. +- **Read form fields** — list a PDF's interactive form fields and current values. +- **Fill forms** — given field/value pairs, produce a filled PDF. +- **Merge** — concatenate or interleave pages from multiple PDFs. + +## How to use + +1. Identify the operation (extract, read-fields, fill, merge). +2. For extract/read-fields: open the input PDF and use the operation's tooling + to produce the structured output. +3. For fill: validate that all required fields are provided, then write the + filled PDF. +4. For merge: confirm page ordering and total page count before writing. +5. Always report the output location and any per-page warnings (low OCR + confidence, missing fields, etc.). + +## References + +See [`references/FORMS.md`](references/FORMS.md) for the field-naming +conventions used by this skill's tooling. diff --git a/examples/sample-archive-source/pdf-processing/references/FORMS.md b/examples/sample-archive-source/pdf-processing/references/FORMS.md new file mode 100644 index 0000000..ccb34c8 --- /dev/null +++ b/examples/sample-archive-source/pdf-processing/references/FORMS.md @@ -0,0 +1,16 @@ +# PDF Form Field Conventions + +The pdf-processing skill expects field names to follow these conventions: + +- **snake_case** for field identifiers (`first_name`, `policy_number`). +- **Type prefixes** for non-text fields: + - `chk_` — checkboxes (boolean values: "Yes"/"No" or true/false) + - `dt_` — dates (ISO 8601: YYYY-MM-DD) + - `sel_` — single-select dropdowns (string matching one of the field's options) + - `num_` — numeric fields (integers or decimals; locale-neutral) +- **Required fields** are flagged with a trailing `*` in the source PDF's tooltip but + appear without it in the field name returned by `read-fields`. + +When filling a form, missing required fields produce a warning per field; the +operation still succeeds with whatever was provided. Use the warnings to decide +whether to surface a follow-up to the user. diff --git a/examples/sample-skills/acme/billing/refunds/SKILL.md b/examples/sample-skills/acme/billing/refunds/SKILL.md new file mode 100644 index 0000000..d1b7f85 --- /dev/null +++ b/examples/sample-skills/acme/billing/refunds/SKILL.md @@ -0,0 +1,43 @@ +--- +name: refunds +description: Process customer refund requests following Acme Corp billing policies. Guides agents through eligibility checks, approval workflows, and customer communication. +metadata: + author: acme-billing-team + version: "1.0" +--- + +# Billing Refunds + +Process customer refund requests following Acme Corp billing policies. + +## When to Use + +- Customer requests a refund for a product or service +- Agent needs to evaluate refund eligibility +- Escalation is needed for refunds above threshold + +## Process + +1. **Verify the customer** — confirm identity and locate the original transaction +2. **Check eligibility** — verify the request falls within the refund window (30 days for products, 14 days for services) +3. **Assess the reason** — categorize as: defective, not as described, changed mind, duplicate charge, or unauthorized +4. **Apply policy rules**: + - Defective/unauthorized: full refund, no questions + - Not as described: full refund with return required + - Changed mind: refund minus 15% restocking fee + - Duplicate charge: full refund automatically +5. **Check approval thresholds**: + - Under $100: auto-approve + - $100–$500: team lead approval + - Over $500: manager approval +6. **Process and communicate** — issue the refund and send confirmation using the email template + +## Templates + +See `templates/refund-email-template.md` for the customer communication template. + +## Important Notes + +- Always log the refund reason for analytics +- Never promise a specific timeline — say "within 5-10 business days" +- If the customer is upset, escalate to a human agent diff --git a/examples/sample-skills/acme/billing/refunds/templates/refund-email-template.md b/examples/sample-skills/acme/billing/refunds/templates/refund-email-template.md new file mode 100644 index 0000000..d7f8298 --- /dev/null +++ b/examples/sample-skills/acme/billing/refunds/templates/refund-email-template.md @@ -0,0 +1,32 @@ +# Refund Confirmation Email Template + +## Subject Line + +Refund Processed — Order #{ORDER_ID} + +## Body + +Dear {CUSTOMER_NAME}, + +We've processed your refund request for order #{ORDER_ID}. + +**Refund Details:** +- Original amount: ${ORIGINAL_AMOUNT} +- Refund amount: ${REFUND_AMOUNT} +- Reason: {REFUND_REASON} +- Reference number: {REFUND_REFERENCE} + +The refund will appear on your original payment method within 5-10 business days. + +If you have any questions, please don't hesitate to reach out to our support team. + +Best regards, +Acme Corp Billing Team + +--- + +## Usage Notes + +- Replace all `{PLACEHOLDER}` values with actual data +- For partial refunds, include a line explaining the difference +- For restocking fee deductions, add: "A 15% restocking fee of ${FEE_AMOUNT} has been applied per our return policy." diff --git a/examples/sample-skills/acme/onboarding/SKILL.md b/examples/sample-skills/acme/onboarding/SKILL.md new file mode 100644 index 0000000..d3e93b3 --- /dev/null +++ b/examples/sample-skills/acme/onboarding/SKILL.md @@ -0,0 +1,47 @@ +--- +name: onboarding +description: Guide new employee onboarding process for Acme Corp. Covers IT setup, team introductions, and first-week checklist. +metadata: + author: acme-hr-team + version: "1.0" +--- + +# Employee Onboarding + +Guide the new employee onboarding process for Acme Corp. + +## When to Use + +- A new employee is joining the team +- HR or a manager needs help with onboarding steps +- New hire asks about their first-week tasks + +## Pre-Start Checklist + +1. **IT Setup** — request laptop, email account, and VPN access +2. **Access provisioning** — add to relevant Slack channels, GitHub org, and internal tools +3. **Workspace** — assign desk/office or ship remote work equipment +4. **Documentation** — prepare offer letter, NDA, and benefits enrollment forms + +## First Day + +1. Welcome meeting with manager (30 min) +2. IT setup walkthrough — verify all accounts and access +3. Team introduction — brief round of introductions +4. Buddy assignment — pair with an experienced team member +5. Company overview — mission, values, org structure + +## First Week + +1. Complete all compliance training modules +2. Attend department orientation +3. Set up development environment (engineering roles) +4. Schedule 1:1s with key stakeholders +5. Review team documentation and README files +6. First small task or PR (engineering roles) + +## 30-60-90 Day Goals + +- **30 days**: Understand team processes, complete initial training, ship first contribution +- **60 days**: Own a small project or feature independently +- **90 days**: Full velocity, participate in on-call rotation (if applicable) diff --git a/examples/sample-skills/code-review/SKILL.md b/examples/sample-skills/code-review/SKILL.md new file mode 100644 index 0000000..953b927 --- /dev/null +++ b/examples/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`. + +## 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/sample-skills/code-review/references/REFERENCE.md b/examples/sample-skills/code-review/references/REFERENCE.md new file mode 100644 index 0000000..718b739 --- /dev/null +++ b/examples/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/sample-skills/git-commit-review/SKILL.md b/examples/sample-skills/git-commit-review/SKILL.md new file mode 100644 index 0000000..c2dc72d --- /dev/null +++ b/examples/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-client/typescript/package-lock.json b/examples/skills-client/typescript/package-lock.json new file mode 100644 index 0000000..cbe1105 --- /dev/null +++ b/examples/skills-client/typescript/package-lock.json @@ -0,0 +1,3505 @@ +{ + "name": "@modelcontextprotocol/skills-sep-example-client", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@modelcontextprotocol/skills-sep-example-client", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/experimental-ext-skills": "file:../../../typescript/sdk", + "@modelcontextprotocol/sdk": "^1.29.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "../../../typescript/sdk": { + "name": "@modelcontextprotocol/experimental-ext-skills", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "tar-stream": "^3.1.7", + "yaml": "^2.7.0", + "yauzl": "^3.2.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "@types/node": "^22.0.0", + "@types/tar-stream": "^3.1.3", + "@types/yauzl": "^2.10.3", + "@types/yazl": "^2.4.6", + "typescript": "^5.7.0", + "vitest": "^3.0.0", + "yazl": "^3.3.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + } + }, + "../../../typescript/sdk/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "../../../typescript/sdk/node_modules/@hono/node-server": { + "version": "1.19.11", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "../../../typescript/sdk/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "dev": true, + "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 + } + } + }, + "../../../typescript/sdk/node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "../../../typescript/sdk/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "../../../typescript/sdk/node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "../../../typescript/sdk/node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/@types/node": { + "version": "22.19.15", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "../../../typescript/sdk/node_modules/@types/tar-stream": { + "version": "3.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "../../../typescript/sdk/node_modules/@types/yauzl": { + "version": "2.10.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "../../../typescript/sdk/node_modules/@types/yazl": { + "version": "2.4.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "../../../typescript/sdk/node_modules/@vitest/expect": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/mocker": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/runner": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/snapshot": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/spy": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/utils": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/accepts": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/ajv": { + "version": "8.18.0", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/ajv-formats": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "../../../typescript/sdk/node_modules/b4a": { + "version": "1.8.1", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/bare-events": { + "version": "2.8.2", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/bare-fs": { + "version": "4.7.1", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/bare-os": { + "version": "3.9.1", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "../../../typescript/sdk/node_modules/bare-path": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "../../../typescript/sdk/node_modules/bare-stream": { + "version": "2.13.1", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/bare-url": { + "version": "2.4.2", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "../../../typescript/sdk/node_modules/body-parser": { + "version": "2.2.2", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/buffer-crc32": { + "version": "0.2.13", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "../../../typescript/sdk/node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/call-bound": { + "version": "1.0.4", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/chai": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "../../../typescript/sdk/node_modules/check-error": { + "version": "2.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "../../../typescript/sdk/node_modules/content-disposition": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/content-type": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/cookie": { + "version": "0.7.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "../../../typescript/sdk/node_modules/cors": { + "version": "2.8.6", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "../../../typescript/sdk/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../../../typescript/sdk/node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/ee-first": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/encodeurl": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/es-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/esbuild": { + "version": "0.27.4", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "../../../typescript/sdk/node_modules/escape-html": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "../../../typescript/sdk/node_modules/etag": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/events-universal": { + "version": "1.0.1", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "../../../typescript/sdk/node_modules/eventsource": { + "version": "3.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "../../../typescript/sdk/node_modules/eventsource-parser": { + "version": "3.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "../../../typescript/sdk/node_modules/expect-type": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "../../../typescript/sdk/node_modules/express": { + "version": "5.2.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/express-rate-limit": { + "version": "8.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "../../../typescript/sdk/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/fast-fifo": { + "version": "1.3.2", + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/fast-uri": { + "version": "3.1.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "../../../typescript/sdk/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/forwarded": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/fresh": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/hono": { + "version": "4.12.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "../../../typescript/sdk/node_modules/http-errors": { + "version": "2.0.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/iconv-lite": { + "version": "0.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/ip-address": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "../../../typescript/sdk/node_modules/ipaddr.js": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "../../../typescript/sdk/node_modules/is-promise": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/jose": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "../../../typescript/sdk/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/json-schema-typed": { + "version": "8.0.2", + "dev": true, + "license": "BSD-2-Clause" + }, + "../../../typescript/sdk/node_modules/loupe": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "../../../typescript/sdk/node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/media-typer": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../../typescript/sdk/node_modules/mime-db": { + "version": "1.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/mime-types": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "../../../typescript/sdk/node_modules/negotiator": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../../typescript/sdk/node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/on-finished": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "../../../typescript/sdk/node_modules/parseurl": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/path-to-regexp": { + "version": "8.3.0", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/pathval": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "../../../typescript/sdk/node_modules/pend": { + "version": "1.2.0", + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../../../typescript/sdk/node_modules/pkce-challenge": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "../../../typescript/sdk/node_modules/postcss": { + "version": "8.5.8", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "../../../typescript/sdk/node_modules/proxy-addr": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "../../../typescript/sdk/node_modules/qs": { + "version": "6.15.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/range-parser": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/raw-body": { + "version": "3.0.2", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../../typescript/sdk/node_modules/rollup": { + "version": "4.59.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.1", + "@rollup/rollup-android-arm64": "4.59.1", + "@rollup/rollup-darwin-arm64": "4.59.1", + "@rollup/rollup-darwin-x64": "4.59.1", + "@rollup/rollup-freebsd-arm64": "4.59.1", + "@rollup/rollup-freebsd-x64": "4.59.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.1", + "@rollup/rollup-linux-arm-musleabihf": "4.59.1", + "@rollup/rollup-linux-arm64-gnu": "4.59.1", + "@rollup/rollup-linux-arm64-musl": "4.59.1", + "@rollup/rollup-linux-loong64-gnu": "4.59.1", + "@rollup/rollup-linux-loong64-musl": "4.59.1", + "@rollup/rollup-linux-ppc64-gnu": "4.59.1", + "@rollup/rollup-linux-ppc64-musl": "4.59.1", + "@rollup/rollup-linux-riscv64-gnu": "4.59.1", + "@rollup/rollup-linux-riscv64-musl": "4.59.1", + "@rollup/rollup-linux-s390x-gnu": "4.59.1", + "@rollup/rollup-linux-x64-gnu": "4.59.1", + "@rollup/rollup-linux-x64-musl": "4.59.1", + "@rollup/rollup-openbsd-x64": "4.59.1", + "@rollup/rollup-openharmony-arm64": "4.59.1", + "@rollup/rollup-win32-arm64-msvc": "4.59.1", + "@rollup/rollup-win32-ia32-msvc": "4.59.1", + "@rollup/rollup-win32-x64-gnu": "4.59.1", + "@rollup/rollup-win32-x64-msvc": "4.59.1", + "fsevents": "~2.3.2" + } + }, + "../../../typescript/sdk/node_modules/router": { + "version": "2.2.0", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/send": { + "version": "1.2.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/serve-static": { + "version": "2.2.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/side-channel": { + "version": "1.1.0", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/side-channel-list": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../../../typescript/sdk/node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/statuses": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/std-env": { + "version": "3.10.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/streamx": { + "version": "2.25.0", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "../../../typescript/sdk/node_modules/strip-literal": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "../../../typescript/sdk/node_modules/tar-stream": { + "version": "3.2.0", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "../../../typescript/sdk/node_modules/teex": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "../../../typescript/sdk/node_modules/text-decoder": { + "version": "1.2.7", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "../../../typescript/sdk/node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "../../../typescript/sdk/node_modules/tinypool": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "../../../typescript/sdk/node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../../../typescript/sdk/node_modules/tinyspy": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../../../typescript/sdk/node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "../../../typescript/sdk/node_modules/type-is": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "../../../typescript/sdk/node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/vary": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/vite": { + "version": "7.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/vite-node": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/vitest": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "../../../typescript/sdk/node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/yaml": { + "version": "2.8.3", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "../../../typescript/sdk/node_modules/yauzl": { + "version": "3.3.0", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "../../../typescript/sdk/node_modules/yazl": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^1.0.0" + } + }, + "../../../typescript/sdk/node_modules/yazl/node_modules/buffer-crc32": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "../../../typescript/sdk/node_modules/zod": { + "version": "3.25.76", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "../../../typescript/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.1", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/experimental-ext-skills": { + "resolved": "../../../typescript/sdk", + "link": true + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "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.15", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "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", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "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", + "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", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "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", + "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", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "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", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "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.3.1", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "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", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "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", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "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", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "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", + "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", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.8", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "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", + "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", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "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", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "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", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "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", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "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", + "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", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "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", + "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", + "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", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "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", + "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", + "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", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/examples/skills-client/typescript/package.json b/examples/skills-client/typescript/package.json new file mode 100644 index 0000000..97030e3 --- /dev/null +++ b/examples/skills-client/typescript/package.json @@ -0,0 +1,26 @@ +{ + "name": "@modelcontextprotocol/skills-sep-example-client", + "version": "0.1.0", + "description": "Reference MCP client demonstrating SEP-2640 (Skills Extension): index discovery, skill-md reads, archive fetch+unpack, and template enumeration", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsx src/index.ts" + }, + "dependencies": { + "@modelcontextprotocol/experimental-ext-skills": "file:../../../typescript/sdk", + "@modelcontextprotocol/sdk": "^1.29.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "license": "Apache-2.0" +} diff --git a/examples/skills-client/typescript/src/index.ts b/examples/skills-client/typescript/src/index.ts new file mode 100644 index 0000000..592c59e --- /dev/null +++ b/examples/skills-client/typescript/src/index.ts @@ -0,0 +1,306 @@ +#!/usr/bin/env node +/** + * Skills Extension SEP — Reference MCP Client + * + * Walks through the client-side surface of SEP-2640 against the bundled + * skills-server example: + * + * 1. READ_RESOURCE_TOOL — host-provided tool schema + * 2. listSkillsFromIndex() — `skill://index.json` discovery + * - entries carry verbatim `frontmatter`, `url`+`digest`, and/or `archives` + * 3. readDirectory() / walkDirectory() — `resources/directory/read` + * 4. listSkills() — fallback via `resources/list` + * 5. readSkillContent() — read an individual SKILL.md + * 6. readSkillArchive() — fetch + safely unpack a .tar.gz + * 7. verifyDigest() — integrity-check a read against the index + * 8. readSkillDocument() — supporting-file flow + * + * Connects to the skills-server via stdio (spawns it as a child process). + * + * @license Apache-2.0 + */ + +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { + READ_RESOURCE_TOOL, + listSkills, + listSkillsFromIndex, + readSkillContent, + readSkillArchive, + readSkillDocument, + buildSkillsSummary, + discoverAndBuildCatalog, + extractSkillUrisFromInstructions, + serverSupportsDirectoryRead, + readDirectory, + walkDirectory, + verifyDigest, +} from "@modelcontextprotocol/experimental-ext-skills/client"; +import { buildSkillUri } from "@modelcontextprotocol/experimental-ext-skills"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +function header(title: string): void { + console.log(`\n${"=".repeat(60)}`); + console.log(` ${title}`); + console.log(`${"=".repeat(60)}\n`); +} + +function subheader(title: string): void { + console.log(`\n--- ${title} ---\n`); +} + +function preview(text: string, maxLines: number): void { + const lines = text.split("\n"); + console.log(lines.slice(0, maxLines).join("\n")); + if (lines.length > maxLines) { + console.log(`\n... (${lines.length - maxLines} more lines)`); + } +} + +async function main(): Promise { + const serverPath = path.resolve( + __dirname, + "../../../skills-server/typescript/dist/index.js", + ); + + console.log("Connecting to skills-sep-example server..."); + console.log(`Server path: ${serverPath}\n`); + + const transport = new StdioClientTransport({ + command: "node", + args: [serverPath], + }); + + const client = new Client( + { name: "skills-sep-example-client", version: "0.1.0" }, + { capabilities: {} }, + ); + + await client.connect(transport); + console.log("Connected!\n"); + + try { + // ----------------------------------------------------------------------- + // 1. Host-provided read_resource tool + // ----------------------------------------------------------------------- + header("1. READ_RESOURCE_TOOL — Host Tool for Model-Driven Loading"); + console.log( + "Per SEP-2640 §Hosts, hosts SHOULD expose a generic resource-reading", + ); + console.log("tool so the model can load skill content (and supporting"); + console.log("files) on demand. The SDK provides the tool schema; the host"); + console.log("wires it to route calls by server name.\n"); + console.log(JSON.stringify(READ_RESOURCE_TOOL, null, 2)); + + // ----------------------------------------------------------------------- + // 2. skill://index.json — covers all entry types + // ----------------------------------------------------------------------- + header("2. listSkillsFromIndex() — skill://index.json Discovery"); + const indexSkills = await listSkillsFromIndex(client); + if (!indexSkills) { + console.log( + "Server does not expose skill://index.json (enumeration is optional)", + ); + } else { + console.log(`Found ${indexSkills.length} skill(s) in index:\n`); + for (const s of indexSkills) { + console.log(` Name: ${s.name}`); + console.log(` Type: ${s.type ?? "skill-md"}`); + console.log(` Skill Path: ${s.skillPath}`); + console.log(` URI: ${s.uri}`); + console.log(` Description: ${s.description}`); + console.log(); + } + } + + // ----------------------------------------------------------------------- + // 3. resources/directory/read — enumerate a skill directory + // ----------------------------------------------------------------------- + header("3. readDirectory() — resources/directory/read enumeration"); + if (!serverSupportsDirectoryRead(client)) { + console.log( + "(server did not declare the directoryRead capability — skipping)", + ); + } else { + const refundsRoot = "skill://acme/billing/refunds"; + console.log(`Listing ${refundsRoot} (metadata only, non-recursive):\n`); + const { resources } = await readDirectory(client, refundsRoot); + for (const child of resources) { + const kind = child.mimeType === "inode/directory" ? "dir " : "file"; + console.log(` [${kind}] ${child.name} (${child.uri})`); + } + + subheader("walkDirectory() — recurse to list every descendant file"); + const files = await walkDirectory(client, refundsRoot); + for (const f of files.sort((a, b) => a.uri.localeCompare(b.uri))) { + console.log(` ${f.uri}`); + } + } + + // ----------------------------------------------------------------------- + // 4. Fallback path — resources/list + // ----------------------------------------------------------------------- + header("4. listSkills() — Fallback via resources/list"); + const listed = await listSkills(client); + console.log(`Found ${listed.length} skill(s) via resources/list:\n`); + for (const s of listed) { + const hasPrefix = s.name !== s.skillPath; + console.log(` ${s.uri}`); + console.log(` name=${s.name}${hasPrefix ? ` path=${s.skillPath}` : ""}`); + } + subheader("buildSkillsSummary() — plain-text catalog for context injection"); + console.log(buildSkillsSummary(listed)); + + // ----------------------------------------------------------------------- + // 5. Read a multi-segment skill (skill-md path) + // ----------------------------------------------------------------------- + header("5. readSkillContent() — Load a Multi-Segment skill-md Skill"); + const refundSkill = listed.find( + (s) => s.skillPath === "acme/billing/refunds", + ); + if (refundSkill) { + console.log(`Reading: ${refundSkill.uri}\n`); + const content = await readSkillContent(client, refundSkill.skillPath); + preview(content, 20); + } else { + console.log("(acme/billing/refunds skill not found)"); + } + + // ----------------------------------------------------------------------- + // 6. Archive distribution — fetch + safely unpack + // ----------------------------------------------------------------------- + header("6. readSkillArchive() — Fetch + Unpack archive distribution"); + const archiveSkill = (indexSkills ?? []).find((s) => s.type === "archive"); + if (archiveSkill) { + console.log(`Archive URI: ${archiveSkill.uri}`); + console.log(`Post-unpack skill path: skill://${archiveSkill.skillPath}/\n`); + const archive = await readSkillArchive(client, archiveSkill.uri); + console.log( + `Unpacked ${archive.files.size} file(s), ${archive.totalSize} bytes total:`, + ); + for (const filePath of [...archive.files.keys()].sort()) { + const size = archive.files.get(filePath)!.length; + console.log(` ${filePath.padEnd(40)} ${size.toString().padStart(6)} bytes`); + } + subheader("Unpacked SKILL.md (first 15 lines)"); + preview(archive.files.get("SKILL.md")!.toString("utf-8"), 15); + } else { + console.log("(no archive entries in this server's index)"); + } + + // ----------------------------------------------------------------------- + // 7. Digest verification — integrity-check a read against the index + // ----------------------------------------------------------------------- + header("7. verifyDigest() — integrity-check against the index digest"); + const verifyTarget = (indexSkills ?? []).find( + (s) => s.type === "skill-md" && s.digest, + ); + if (verifyTarget) { + console.log(`Skill: ${verifyTarget.uri}`); + console.log(`Index digest: ${verifyTarget.digest}\n`); + const content = await readSkillContent(client, verifyTarget.skillPath); + const ok = verifyDigest(content, verifyTarget.digest!); + console.log( + ok + ? "Content matches the index digest ✓ (SEP-2640: hosts MUST verify)" + : "Content does NOT match the index digest ✗ — possible tamper/drift", + ); + console.log( + "\nNote: the index digest is over the SKILL.md raw bytes, and SKILL.md", + ); + console.log( + "is UTF-8, so hashing the received text (UTF-8) matches byte-for-byte.", + ); + } else { + console.log("(no skill-md entry with a digest to verify)"); + } + + // ----------------------------------------------------------------------- + // 8. Server `instructions` — third SEP discovery path + // ----------------------------------------------------------------------- + header("8. Server instructions — third discovery path"); + const serverInstructions = client.getInstructions(); + console.log(`Server instructions:\n${serverInstructions ?? "(none)"}\n`); + const namedUris = extractSkillUrisFromInstructions(serverInstructions); + console.log( + `URIs the server names in instructions: ${ + namedUris.length ? namedUris.join(", ") : "(none)" + }`, + ); + console.log( + "discoverSkills() merges these URIs with skill://index.json hits,", + ); + console.log("deduplicated by URI."); + + // ----------------------------------------------------------------------- + // 9. discoverAndBuildCatalog — system-prompt catalog with per-entry + // ----------------------------------------------------------------------- + header("9. discoverAndBuildCatalog() — system-prompt catalog"); + // Two opt-ins on top of the SEP-prescribed defaults: + // - `instructions: true` enables the SEP's third discovery path + // - `serverInEntries: true` injects per entry, the + // host SKILL.md's recommended placement for the model to copy + // alongside the URI when calling a (server, uri) reader tool. + // Both are off by default since neither is in SEP-2640 itself. + const { skills: catalogSkills, catalog } = await discoverAndBuildCatalog( + client, + { + serverName: "skills-sep-example", + instructions: true, + serverInEntries: true, + }, + ); + console.log( + `Catalog covers ${catalogSkills.length} skill(s) (index + instructions).\n`, + ); + console.log( + "With serverInEntries: true, the server name is also placed inside", + ); + console.log("each entry so the model can copy it next to the URI:"); + console.log(); + preview(catalog, 30); + + // ----------------------------------------------------------------------- + // 10. Supporting-file flow + // ----------------------------------------------------------------------- + header("10. readSkillDocument() — supporting-file flow"); + if (refundSkill) { + const docPath = "templates/refund-email-template.md"; + const docUri = buildSkillUri(refundSkill.skillPath, docPath); + console.log(`Reading: ${docUri}\n`); + const doc = await readSkillDocument( + client, + refundSkill.skillPath, + docPath, + ); + if (doc.text) preview(doc.text, 15); + } + + // ----------------------------------------------------------------------- + // Summary + // ----------------------------------------------------------------------- + header("Demo Complete"); + console.log("Demonstrated SEP-2640 features:"); + console.log(" [SEP-2640] Extension declaration (io.modelcontextprotocol/skills)"); + console.log(" [SEP-2640] skill:// URI scheme + multi-segment paths"); + console.log(" [SEP-2640] skill://index.json discovery (verbatim frontmatter, url+digest, archives)"); + console.log(" [SEP-2640] resources/directory/read enumeration (directoryRead capability)"); + console.log(" [SEP-2640] Server instructions — third discovery path"); + console.log(" [SEP-2640] Archive fetch + safe unpack (.tar.gz, archive safety)"); + console.log(" [SEP-2640] Digest verification against the index"); + console.log(" [Hosts] read_resource tool surface + per-entry in catalog"); + console.log(" [Hosts] supporting-file flow"); + console.log(); + } finally { + await client.close(); + } +} + +main().catch((error) => { + console.error("Fatal error:", error); + process.exit(1); +}); diff --git a/examples/skills-client/typescript/tsconfig.json b/examples/skills-client/typescript/tsconfig.json new file mode 100644 index 0000000..0408ba3 --- /dev/null +++ b/examples/skills-client/typescript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/examples/skills-server/typescript/package-lock.json b/examples/skills-server/typescript/package-lock.json new file mode 100644 index 0000000..320f616 --- /dev/null +++ b/examples/skills-server/typescript/package-lock.json @@ -0,0 +1,3678 @@ +{ + "name": "@modelcontextprotocol/skills-sep-example-server", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@modelcontextprotocol/skills-sep-example-server", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "@modelcontextprotocol/experimental-ext-skills": "file:../../../typescript/sdk", + "@modelcontextprotocol/sdk": "^1.29.0", + "tar-stream": "^3.1.7" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/tar-stream": "^3.1.3", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "../../../typescript/sdk": { + "name": "@modelcontextprotocol/experimental-ext-skills", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "tar-stream": "^3.1.7", + "yaml": "^2.7.0", + "yauzl": "^3.2.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "@types/node": "^22.0.0", + "@types/tar-stream": "^3.1.3", + "@types/yauzl": "^2.10.3", + "@types/yazl": "^2.4.6", + "typescript": "^5.7.0", + "vitest": "^3.0.0", + "yazl": "^3.3.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + } + }, + "../../../typescript/sdk/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "../../../typescript/sdk/node_modules/@hono/node-server": { + "version": "1.19.11", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "../../../typescript/sdk/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "dev": true, + "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 + } + } + }, + "../../../typescript/sdk/node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "../../../typescript/sdk/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "../../../typescript/sdk/node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "../../../typescript/sdk/node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/@types/node": { + "version": "22.19.15", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "../../../typescript/sdk/node_modules/@types/tar-stream": { + "version": "3.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "../../../typescript/sdk/node_modules/@types/yauzl": { + "version": "2.10.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "../../../typescript/sdk/node_modules/@types/yazl": { + "version": "2.4.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "../../../typescript/sdk/node_modules/@vitest/expect": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/mocker": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/runner": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/snapshot": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/spy": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/@vitest/utils": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/accepts": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/ajv": { + "version": "8.18.0", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/ajv-formats": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "../../../typescript/sdk/node_modules/b4a": { + "version": "1.8.1", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/bare-events": { + "version": "2.8.2", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/bare-fs": { + "version": "4.7.1", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/bare-os": { + "version": "3.9.1", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "../../../typescript/sdk/node_modules/bare-path": { + "version": "3.0.0", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "../../../typescript/sdk/node_modules/bare-stream": { + "version": "2.13.1", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/bare-url": { + "version": "2.4.2", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "../../../typescript/sdk/node_modules/body-parser": { + "version": "2.2.2", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/buffer-crc32": { + "version": "0.2.13", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "../../../typescript/sdk/node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/call-bound": { + "version": "1.0.4", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/chai": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "../../../typescript/sdk/node_modules/check-error": { + "version": "2.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "../../../typescript/sdk/node_modules/content-disposition": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/content-type": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/cookie": { + "version": "0.7.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "../../../typescript/sdk/node_modules/cors": { + "version": "2.8.6", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "../../../typescript/sdk/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../../../typescript/sdk/node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/ee-first": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/encodeurl": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/es-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/esbuild": { + "version": "0.27.4", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "../../../typescript/sdk/node_modules/escape-html": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "../../../typescript/sdk/node_modules/etag": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/events-universal": { + "version": "1.0.1", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "../../../typescript/sdk/node_modules/eventsource": { + "version": "3.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "../../../typescript/sdk/node_modules/eventsource-parser": { + "version": "3.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "../../../typescript/sdk/node_modules/expect-type": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "../../../typescript/sdk/node_modules/express": { + "version": "5.2.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/express-rate-limit": { + "version": "8.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "../../../typescript/sdk/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/fast-fifo": { + "version": "1.3.2", + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/fast-uri": { + "version": "3.1.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "../../../typescript/sdk/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/finalhandler": { + "version": "2.1.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/forwarded": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/fresh": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/hono": { + "version": "4.12.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "../../../typescript/sdk/node_modules/http-errors": { + "version": "2.0.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/iconv-lite": { + "version": "0.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/ip-address": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "../../../typescript/sdk/node_modules/ipaddr.js": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "../../../typescript/sdk/node_modules/is-promise": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/jose": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "../../../typescript/sdk/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/json-schema-typed": { + "version": "8.0.2", + "dev": true, + "license": "BSD-2-Clause" + }, + "../../../typescript/sdk/node_modules/loupe": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "../../../typescript/sdk/node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "../../../typescript/sdk/node_modules/media-typer": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../../../typescript/sdk/node_modules/mime-db": { + "version": "1.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/mime-types": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "../../../typescript/sdk/node_modules/negotiator": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../../typescript/sdk/node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/on-finished": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "../../../typescript/sdk/node_modules/parseurl": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/path-to-regexp": { + "version": "8.3.0", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "../../../typescript/sdk/node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/pathval": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "../../../typescript/sdk/node_modules/pend": { + "version": "1.2.0", + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/picomatch": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../../../typescript/sdk/node_modules/pkce-challenge": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "../../../typescript/sdk/node_modules/postcss": { + "version": "8.5.8", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "../../../typescript/sdk/node_modules/proxy-addr": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "../../../typescript/sdk/node_modules/qs": { + "version": "6.15.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/range-parser": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/raw-body": { + "version": "3.0.2", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../../../typescript/sdk/node_modules/rollup": { + "version": "4.59.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.1", + "@rollup/rollup-android-arm64": "4.59.1", + "@rollup/rollup-darwin-arm64": "4.59.1", + "@rollup/rollup-darwin-x64": "4.59.1", + "@rollup/rollup-freebsd-arm64": "4.59.1", + "@rollup/rollup-freebsd-x64": "4.59.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.1", + "@rollup/rollup-linux-arm-musleabihf": "4.59.1", + "@rollup/rollup-linux-arm64-gnu": "4.59.1", + "@rollup/rollup-linux-arm64-musl": "4.59.1", + "@rollup/rollup-linux-loong64-gnu": "4.59.1", + "@rollup/rollup-linux-loong64-musl": "4.59.1", + "@rollup/rollup-linux-ppc64-gnu": "4.59.1", + "@rollup/rollup-linux-ppc64-musl": "4.59.1", + "@rollup/rollup-linux-riscv64-gnu": "4.59.1", + "@rollup/rollup-linux-riscv64-musl": "4.59.1", + "@rollup/rollup-linux-s390x-gnu": "4.59.1", + "@rollup/rollup-linux-x64-gnu": "4.59.1", + "@rollup/rollup-linux-x64-musl": "4.59.1", + "@rollup/rollup-openbsd-x64": "4.59.1", + "@rollup/rollup-openharmony-arm64": "4.59.1", + "@rollup/rollup-win32-arm64-msvc": "4.59.1", + "@rollup/rollup-win32-ia32-msvc": "4.59.1", + "@rollup/rollup-win32-x64-gnu": "4.59.1", + "@rollup/rollup-win32-x64-msvc": "4.59.1", + "fsevents": "~2.3.2" + } + }, + "../../../typescript/sdk/node_modules/router": { + "version": "2.2.0", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/send": { + "version": "1.2.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/serve-static": { + "version": "2.2.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/side-channel": { + "version": "1.1.0", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/side-channel-list": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../../../typescript/sdk/node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/side-channel-weakmap": { + "version": "1.0.2", + "dev": true, + "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" + } + }, + "../../../typescript/sdk/node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../../../typescript/sdk/node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/statuses": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/std-env": { + "version": "3.10.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/streamx": { + "version": "2.25.0", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "../../../typescript/sdk/node_modules/strip-literal": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "../../../typescript/sdk/node_modules/tar-stream": { + "version": "3.2.0", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "../../../typescript/sdk/node_modules/teex": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "../../../typescript/sdk/node_modules/text-decoder": { + "version": "1.2.7", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "../../../typescript/sdk/node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "../../../typescript/sdk/node_modules/tinypool": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "../../../typescript/sdk/node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../../../typescript/sdk/node_modules/tinyspy": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../../../typescript/sdk/node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "../../../typescript/sdk/node_modules/type-is": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "../../../typescript/sdk/node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "../../../typescript/sdk/node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + }, + "../../../typescript/sdk/node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/vary": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "../../../typescript/sdk/node_modules/vite": { + "version": "7.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/vite-node": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../../../typescript/sdk/node_modules/vitest": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "../../../typescript/sdk/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "../../../typescript/sdk/node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "../../../typescript/sdk/node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "../../../typescript/sdk/node_modules/yaml": { + "version": "2.8.3", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "../../../typescript/sdk/node_modules/yauzl": { + "version": "3.3.0", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "../../../typescript/sdk/node_modules/yazl": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^1.0.0" + } + }, + "../../../typescript/sdk/node_modules/yazl/node_modules/buffer-crc32": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "../../../typescript/sdk/node_modules/zod": { + "version": "3.25.76", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "../../../typescript/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.1", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/experimental-ext-skills": { + "resolved": "../../../typescript/sdk", + "link": true + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "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.15", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/tar-stream": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.4.tgz", + "integrity": "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "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", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "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", + "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", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "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", + "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", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "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", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "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.3.1", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "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", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "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", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "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", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "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", + "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", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.8", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "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", + "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", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "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", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "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", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "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", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "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", + "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", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "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", + "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", + "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", + "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", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "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", + "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", + "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", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/examples/skills-server/typescript/package.json b/examples/skills-server/typescript/package.json new file mode 100644 index 0000000..7008642 --- /dev/null +++ b/examples/skills-server/typescript/package.json @@ -0,0 +1,28 @@ +{ + "name": "@modelcontextprotocol/skills-sep-example-server", + "version": "0.1.0", + "description": "Reference MCP server demonstrating SEP-2640 (Skills Extension): individual-file skills, archive distribution, and resource templates", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsx src/index.ts" + }, + "dependencies": { + "@modelcontextprotocol/experimental-ext-skills": "file:../../../typescript/sdk", + "@modelcontextprotocol/sdk": "^1.29.0", + "tar-stream": "^3.1.7" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@types/tar-stream": "^3.1.3", + "tsx": "^4.19.0", + "typescript": "^5.7.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "license": "Apache-2.0" +} diff --git a/examples/skills-server/typescript/src/index.ts b/examples/skills-server/typescript/src/index.ts new file mode 100644 index 0000000..feddf37 --- /dev/null +++ b/examples/skills-server/typescript/src/index.ts @@ -0,0 +1,154 @@ +#!/usr/bin/env node +/** + * Skills Extension SEP — Reference MCP Server + * + * Demonstrates the SEP-2640 `skill://index.json` index, whose entries are + * type-less: each carries the skill's verbatim `frontmatter` plus a `url` + * (with `digest`) and/or an `archives` array. This server exposes both + * forms: + * + * - individually-served file skills (entry has `url` + `digest`) + * - an archive distribution (entry has an `archives` array) + * + * Plus the SEP-2640 capability declaration (`io.modelcontextprotocol/skills` + * with `directoryRead: true`), the `resources/directory/read` method for + * enumerating skill directories, and multi-segment skill paths. + * + * Resource layout: + * skill://index.json — discovery index + * skill://code-review/SKILL.md — file skill (single segment) + * skill://git-commit-review/SKILL.md — file skill + * skill://acme/onboarding/SKILL.md — file skill (multi-segment) + * skill://acme/billing/refunds/SKILL.md — file skill (multi-segment) + * skill://pdf-processing.tar.gz — archive distribution + * + * @license Apache-2.0 + */ + +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseArgs } from "node:util"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + discoverSkills, + registerSkillResources, + declareSkillsExtension, +} from "@modelcontextprotocol/experimental-ext-skills/server"; + +import { packTarGz } from "./pack-archive.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const { positionals } = parseArgs({ + args: process.argv.slice(2), + allowPositionals: true, +}); + +// Default to the bundled sample-skills directory if no path is provided. +const skillsDir = positionals[0] + ? path.resolve(positionals[0]) + : path.resolve(__dirname, "../../../sample-skills"); +const archiveSourceDir = path.resolve( + __dirname, + "../../../sample-archive-source", +); + +// --------------------------------------------------------------------------- +// Discover individually-served skills +// --------------------------------------------------------------------------- + +const skillMap = discoverSkills(skillsDir); +console.error( + `[skills-server] Discovered ${skillMap.size} file skill(s) in ${skillsDir}`, +); +for (const [skillPath, skill] of skillMap) { + console.error(` - skill://${skillPath}/SKILL.md (name: "${skill.name}")`); +} + +// --------------------------------------------------------------------------- +// Build the archive-distributed skill (pdf-processing.tar.gz) +// --------------------------------------------------------------------------- + +const pdfSourceDir = path.join(archiveSourceDir, "pdf-processing"); +let archivePath: string | undefined; +if (fs.existsSync(pdfSourceDir)) { + const archiveBytes = await packTarGz(pdfSourceDir); + // Write to a tempfile so registerSkillResources() can mmap it via fs.readFileSync(). + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), "skills-sep-example-archive-"), + ); + archivePath = path.join(tmpDir, "pdf-processing.tar.gz"); + fs.writeFileSync(archivePath, archiveBytes); + console.error( + `[skills-server] Packed pdf-processing → ${archivePath} (${archiveBytes.length} bytes)`, + ); +} else { + console.error( + `[skills-server] (no archive source at ${pdfSourceDir}; skipping archive demo)`, + ); +} + +// --------------------------------------------------------------------------- +// Create MCP server and declare the extension (SEP-2640) +// --------------------------------------------------------------------------- + +// Server `instructions` is the SEP's third discovery path — a host MAY mine +// it for skill URIs the server explicitly names (separate from the index). +// We name git-commit-review here as a demo URI; it would also still appear +// via the index, which is fine: the client dedups by URI. +const serverInstructions = [ + "This server exposes Agent Skills under the skill:// scheme.", + "When reviewing a commit, read skill://git-commit-review/SKILL.md first.", +].join("\n"); + +const server = new McpServer( + { name: "skills-sep-example", version: "0.1.0" }, + { capabilities: { resources: {} }, instructions: serverInstructions }, +); +// Declare the extension and advertise the directory-read capability. This +// MUST happen before connect() — capabilities ship in the initialize +// handshake — and is paired with `directoryRead: true` below. +declareSkillsExtension(server.server, { directoryRead: true }); + +// --------------------------------------------------------------------------- +// Register all resources via the SDK +// --------------------------------------------------------------------------- + +registerSkillResources(server, skillMap, skillsDir, { + template: true, + // Implement resources/directory/read so hosts can enumerate skill dirs. + directoryRead: true, + // Archive entry — single resource that unpacks to skill://pdf-processing/ + archives: archivePath + ? [ + { + name: "pdf-processing", + description: + "Extract text and form data from PDFs, fill PDF forms, and merge multi-page documents.", + skillPath: "pdf-processing", + archivePath, + // format inferred from .tar.gz extension + }, + ] + : [], +}); + +console.error( + "[skills-server] Extension: io.modelcontextprotocol/skills (directoryRead: true)", +); +console.error( + `[skills-server] Index will list: ${skillMap.size} file skill(s) + ${ + archivePath ? 1 : 0 + } archive entry`, +); + +// --------------------------------------------------------------------------- +// Connect via stdio +// --------------------------------------------------------------------------- + +const transport = new StdioServerTransport(); +await server.connect(transport); +console.error("[skills-server] Connected via stdio"); diff --git a/examples/skills-server/typescript/src/pack-archive.ts b/examples/skills-server/typescript/src/pack-archive.ts new file mode 100644 index 0000000..e614c76 --- /dev/null +++ b/examples/skills-server/typescript/src/pack-archive.ts @@ -0,0 +1,66 @@ +/** + * Pack a directory tree into a `.tar.gz` Buffer in memory. + * + * Used by the example server to demonstrate SEP-2640 archive distribution: + * the source skill directory is packed at startup and registered as a + * single archive resource via `registerSkillResources({ archives: [...] })`. + * + * The SDK itself does not ship a packer — that would conflate "host SDK" + * with "skill-author tooling." For real deployments, archives are + * pre-built outside the server. This helper exists so the example is + * self-contained. + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as zlib from "node:zlib"; +import { pack as tarPack } from "tar-stream"; + +/** + * Recursively collect files under `dir`, returning paths relative to `dir`. + * SKILL.md is placed first so archive readers see it without seeking. + */ +function listFilesRelative(dir: string): string[] { + const out: string[] = []; + const walk = (cur: string, prefix: string): void => { + for (const entry of fs.readdirSync(cur, { withFileTypes: true })) { + const full = path.join(cur, entry.name); + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + walk(full, rel); + } else if (entry.isFile()) { + out.push(rel); + } + } + }; + walk(dir, ""); + return out.sort((a, b) => { + if (a === "SKILL.md") return -1; + if (b === "SKILL.md") return 1; + return a.localeCompare(b); + }); +} + +/** + * Pack `sourceDir` into a tar.gz Buffer. + * + * The archive contents are placed at the archive root (no wrapper + * directory) per SEP-2640. + */ +export async function packTarGz(sourceDir: string): Promise { + const files = listFilesRelative(sourceDir); + const pack = tarPack(); + + for (const rel of files) { + const full = path.join(sourceDir, rel); + const data = fs.readFileSync(full); + pack.entry({ name: rel, size: data.length }, data); + } + pack.finalize(); + + const tarChunks: Buffer[] = []; + for await (const chunk of pack as unknown as AsyncIterable) { + tarChunks.push(chunk); + } + return zlib.gzipSync(Buffer.concat(tarChunks)); +} diff --git a/examples/skills-server/typescript/tsconfig.json b/examples/skills-server/typescript/tsconfig.json new file mode 100644 index 0000000..0408ba3 --- /dev/null +++ b/examples/skills-server/typescript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/typescript/sdk/CLAUDE.md b/typescript/sdk/CLAUDE.md new file mode 100644 index 0000000..42abe6e --- /dev/null +++ b/typescript/sdk/CLAUDE.md @@ -0,0 +1,118 @@ +# Skills Extension SDK + +## Design philosophy + +This SDK implements three layers: + +1. **Protocol layer** — Types, URI scheme, index format, constants. Maps directly to the SEP spec. Lives in `types.ts`, `uri.ts`, `mime.ts`. + +2. **API layer** — Direct wrappers around single protocol operations. Each function maps to one spec concept: `listSkillsFromIndex()` reads `skill://index.json`, `readSkillUri()` calls `resources/read`, `registerSkillResources()` registers MCP resources. Lives in `_client.ts`, `_server.ts`, `resource-extensions.ts`. + +3. **Ergonomic layer** — Chains API-layer calls with opinionated defaults and fallback logic. `discoverSkills()` tries index then falls back to `resources/list`. `discoverAndBuildCatalog()` chains discovery into catalog building with a sensible `toolName` default. + +The main principle is to **make simple things easy and complex things possible.** The ergonomic layer handles the 80% case; the API layer remains available for full control. + +## Scheme agnosticism + +Index entries may use any URI scheme. Functions that accept URIs from the index (`readSkillUri`, `discoverSkills`, `buildSkillsCatalog`) are scheme-agnostic. Functions that construct URIs from skill paths (`readSkillContent`, `readSkillDocument`) always produce `skill://` and are documented accordingly. + +Per SEP-2640, the structural constraints on `` apply *regardless of scheme*. `extractSkillPathFromUri()` extracts the path between `://` and `/SKILL.md` for any URI; `listSkillsFromIndex()` and `listSkillsFromInstructions()` use it to populate `SkillSummary.skillPath`, falling back to the entry's `name` only when the URL doesn't match `:///SKILL.md`. The model-facing `skillPath` is therefore the SEP-defined locator across schemes. + +## Index format (SEP-2640) + +The `skill://index.json` schema is the WG's own (decoupled from the +agentskills.io `.well-known` discovery format) and carries **no** `$schema` / +version marker. Each entry is **type-less**: + +- `frontmatter` — the skill's SKILL.md frontmatter copied **verbatim** as JSON. + Name and description live here, not as top-level entry fields. The server + captures the full block in `discoverSkills` (`SkillMetadata.frontmatter`); + the client reads `frontmatter.name` / `frontmatter.description`. +- `url` + `digest` — present when the skill is served as individual files; + `digest` is `sha256:{hex}` over the SKILL.md raw bytes (computed once in + `discoverSkills` from the bytes it already reads). +- `archives` — an array of `{ url, mimeType, digest }`, one per packed form. + +Every entry MUST have a `url`, a non-empty `archives`, or both; +`listSkillsFromIndex` skips entries with neither and derives +`SkillSummary.type` from the entry shape. There is no `mcp-resource-template` +entry type and no parameterized-template serving feature — both were removed +when the index schema decoupled from `.well-known`. The catch-all +`skill://{+skillFilePath}` template (for supporting files) is unrelated and +stays. + +## Directory enumeration (`resources/directory/read`) + +`directory.ts` owns the SEP-2640 method. `buildDirectoryTree(skillMap)` derives +every directory implied by skill paths + scanned documents and its direct +children (files with their mime; subdirectories with `inode/directory`); +`makeDirectoryReadHandler` serves a paginated, metadata-only, non-recursive +listing and throws `McpError(InvalidParams)` (`-32602`) on a non-directory or +unknown URI. It is registered on the **low-level** `Server` via +`server.server.setRequestHandler(DirectoryReadRequestSchema, …)` because it is +an extension method with no high-level `McpServer` wrapper. + +The capability is opt-in and must be declared in **two** places that the SDK +deliberately keeps separate: `declareSkillsExtension(server, { directoryRead: +true })` advertises it in the initialize handshake (so it must run before +`connect()`), and `registerSkillResources(…, { directoryRead: true })` installs +the handler. The client gates calls with `serverSupportsDirectoryRead()` (reads +the declared capability) before issuing `readDirectory()` / `walkDirectory()`. + +## `_meta` policy + +The SDK never auto-projects frontmatter into resource `_meta`. Per `docs/skill-meta-keys.md`, skill-level semantics (version, allowed-tools, invocation, etc.) belong in frontmatter — the resource content — not duplicated on the resource. `SkillMetadata.meta` is the opt-in surface for transport-layer concerns that have no frontmatter equivalent (provenance, content-integrity hashes). The SDK only sets `_meta` when the caller fills this field. + +## Discovery paths + +`discoverSkills()` covers the SEP's three discovery paths, but mines `instructions` only on opt-in: + +1. `skill://index.json` (authoritative, scheme-agnostic) — primary, always tried +2. Server `instructions` (URIs the server names) — opt in with `{ instructions: true }`; read via `client.getInstructions()` when the structural `SkillsClient` exposes it; merged with index entries deduplicated by URI. Pass `{ extractor }` to override the built-in regex. +3. `resources/list` (skill:// scheme only) — fallback when both above are empty + +The default is **opt-out** for `instructions` mining because most servers don't name skill URIs there, and the per-URI read round-trips would be wasted. Hosts that want the third path enable it explicitly per the SEP narrative. + +Index hits don't suppress instructions mining when opted in; the two are merged. This handles servers that publish a base catalog *and* call out specific URIs in their instructions. + +## Per-entry `` in catalog + +`generateSkillsXMLFromSummaries(skills, { serverName, serverInEntries: true })` injects `` inside each ``. **Off by default**: per-entry placement is host-narrative from the host SKILL.md, not SEP-2640, so we don't impose it on every consumer. Hosts using `(server, uri)` reader tools opt in for the activation-reliability lift; hosts whose readers are server-scoped leave it off. + +The wrapper-level mention of `serverName` in `buildSkillsCatalog`'s prose is a separate concern, controlled by `serverName` presence alone. + +## Defaults policy + +Behaviors normatively prescribed by SEP-2640 are on by default. Behaviors that come from the WIP host/server SKILL.md narrative or related WG docs (`skill-meta-keys.md`) but aren't in SEP-2640 are opt-in: + +| Behavior | Source | Default | +|---|---|---| +| `skill://index.json` discovery (client) | SEP-2640 | always on | +| `skill://index.json` registration (server) | SEP-2640 SHOULD | on; opt-out via `index: false` for unenumerable catalogs | +| `resources/list` fallback | SEP-2640 | always on | +| Final-segment-equals-name validation | SEP-2640 | always enforced | +| Skill name `^[a-z0-9-]+$` validation | SEP-2640 + agentskills.io | always enforced | +| Archive safety | SEP-2640 | always enforced | +| Per-entry `digest` in index (`sha256:`) | SEP-2640 | always emitted | +| Digest verification on read | SEP-2640 MUST | opt-in (`verifyDigest` / `readSkillUriVerified`) | +| `resources/directory/read` handler | SEP-2640 | opt-in (`directoryRead: true` + `declareSkillsExtension`) | +| `instructions` discovery path | host SKILL.md | opt-in (`instructions: true`) | +| Custom URI extractor | SDK | opt-in (`extractor`) | +| Per-entry `` in catalog XML | host SKILL.md | opt-in (`serverInEntries: true`) | +| Custom `_meta` per skill | `skill-meta-keys.md` | opt-in (caller fills `meta`) | +| `serverName` in catalog prose wrapper | host SKILL.md | optional (set `serverName`) | +| No-nesting constraint | PR #70 | always enforced (correctness) | +| Catch-all supporting-files template | SDK mechanism | on (delivers SEP-prescribed function) | +| `audience: ["assistant"]` annotation | `skill-meta-keys.md` | default (overridable) | + +## Structural typing + +`SkillsClient` and `SkillsServer` are structural interfaces, not re-exports of the MCP SDK's concrete classes. This avoids type incompatibilities when consumers have a different version of `@modelcontextprotocol/sdk` installed. + +## Subpath exports + +- `experimental-ext-skills` — shared types, URI utilities +- `experimental-ext-skills/client` — client-side discovery, reading, catalog building +- `experimental-ext-skills/server` — server-side discovery, resource registration + +Client and server exports are intentionally separate. Types used by exported functions should be re-exported from the same subpath so users don't need multiple imports. diff --git a/typescript/sdk/README.md b/typescript/sdk/README.md new file mode 100644 index 0000000..2cec0f6 --- /dev/null +++ b/typescript/sdk/README.md @@ -0,0 +1,389 @@ +# @modelcontextprotocol/experimental-ext-skills + +TypeScript SDK for the [Skills Extension SEP](https://github.com/modelcontextprotocol/experimental-ext-skills/pull/69) — serves agent skills as `skill://` resources over MCP. + +> **Experimental.** Published as [`@modelcontextprotocol/experimental-ext-skills`](https://www.npmjs.com/package/@modelcontextprotocol/experimental-ext-skills) for testing while the spec is in draft. + +## Install + +```bash +npm install @modelcontextprotocol/experimental-ext-skills @modelcontextprotocol/sdk +``` + +## Subpath exports + +| Import path | Purpose | +|---|---| +| `@modelcontextprotocol/experimental-ext-skills` | Shared types, URI utilities, constants | +| `@modelcontextprotocol/experimental-ext-skills/server` | Server-side: discover skills, register MCP resources | +| `@modelcontextprotocol/experimental-ext-skills/client` | Client-side: list skills, read content, build summaries | + +## Server usage + +Discover skills from a directory of `SKILL.md` files and serve them as MCP resources: + +```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + discoverSkills, + registerSkillResources, + declareSkillsExtension, +} from "@modelcontextprotocol/experimental-ext-skills/server"; + +// Recursively scan a directory for SKILL.md files +const skillMap = discoverSkills("./skills"); + +// Create server and declare the skills extension (SEP-2640). +// Pass { directoryRead: true } to advertise resources/directory/read. +const server = new McpServer( + { name: "my-server", version: "1.0.0" }, + { capabilities: { resources: {} } }, +); +declareSkillsExtension(server.server, { directoryRead: true }); + +// Register all skill resources (SKILL.md, index, supporting-file template, +// and the resources/directory/read handler). +registerSkillResources(server, skillMap, "./skills", { + template: true, // enable resource template for supporting files + directoryRead: true, // implement resources/directory/read (pairs with the declaration above) + // audience defaults to ["assistant"] — skills consumed only by the model + // use ["user", "assistant"] for skills also shown in a skill browser UI +}); + +await server.connect(new StdioServerTransport()); +``` + +> **Directory enumeration is opt-in.** Declaring `directoryRead: true` and +> passing `{ directoryRead: true }` to `registerSkillResources` are a pair: +> the first advertises the capability in the initialize handshake (so it must +> run before `connect()`), the second installs the handler. + +### Skill directory structure + +``` +skills/ + code-review/ + SKILL.md # Required: YAML frontmatter + markdown body + references/ + REFERENCE.md # Optional: supporting files + acme/billing/refunds/ + SKILL.md # Multi-segment paths supported + templates/ + refund-email-template.md +``` + +Each `SKILL.md` requires YAML frontmatter with `name` and `description`: + +```yaml +--- +name: code-review +description: Review code changes for quality and correctness +--- + +# Code Review + +Instructions for the agent... +``` + +### Registered resources + +The server registers, per the SEP: + +- `skill://{skillPath}/SKILL.md` — one per discovered skill +- `skill://index.json` — discovery index (file skills + archive distributions) +- `skill://{+skillFilePath}` — catch-all resource template for supporting + files (optional, on by default) +- A `resources/directory/read` handler when `directoryRead: true` (see + *Directory enumeration* below) + +### Resource annotations + +All resources include `annotations` with `audience`, `priority`, and `lastModified` (see [`skill-meta-keys.md`](../../docs/skill-meta-keys.md)): + +- **`audience`** defaults to `["assistant"]`. Override globally via options, or per-skill via `SkillMetadata.audience`: + +```typescript +// Global default for all skills +registerSkillResources(server, skillMap, "./skills", { + audience: ["user", "assistant"], +}); + +// Per-skill override (e.g., set from frontmatter or config) +const skillMap = discoverSkills("./skills"); +for (const skill of skillMap.values()) { + skill.audience = ["user", "assistant"]; +} +``` + +- **`priority`** is set per resource type: 1.0 (SKILL.md), 0.9 (archive), 0.8 (index), 0.2 (supporting-file catch-all) +- **`lastModified`** uses per-skill mtime for SKILL.md, archive mtime for archives, and the most recent mtime across all skills for the index +- **`size`** is set on all resources except the catch-all template (which varies per request) + +### Directory enumeration + +When `directoryRead: true`, the server implements the SEP-2640 +`resources/directory/read` method so hosts can enumerate the files under a +skill directory without knowing every URI up front — an `ls`-style, +metadata-only, paginated, non-recursive listing. Directories are identified +by `mimeType: "inode/directory"`. + +```typescript +declareSkillsExtension(server.server, { directoryRead: true }); // before connect() +registerSkillResources(server, skillMap, "./skills", { directoryRead: true }); +``` + +The handler is backed by the in-memory skill map (skill paths + scanned +supporting documents), so it covers skills served as individual files. +Archive-distributed skills are opaque to the server and are not walked. On the +client, gate calls with `serverSupportsDirectoryRead(client)` and use +`readDirectory()` / `walkDirectory()` (see *Client usage*). + +### Custom `_meta` per skill + +Per [`skill-meta-keys.md`](../../docs/skill-meta-keys.md), most skills do **not** need `_meta` — name, description, version, allowed-tools, and other skill-level semantics belong in frontmatter (the resource body), not duplicated on the resource. The SDK reflects this: it never auto-projects frontmatter into `_meta`. When you need transport-layer metadata that has no frontmatter equivalent (provenance the host needs without reading content, content-integrity hashes, etc.), set it on the discovered `SkillMetadata.meta`: + +```typescript +const skillMap = discoverSkills("./skills"); +const refunds = skillMap.get("acme/billing/refunds"); +if (refunds) { + refunds.meta = { + "io.modelcontextprotocol.skills/provenance": "acme/billing-team", + }; +} +registerSkillResources(server, skillMap, "./skills"); +``` + +The SDK passes `meta` through to the SKILL.md resource's `_meta` field; keys SHOULD use the `io.modelcontextprotocol.skills/` reverse-domain prefix. + +### Archive distribution + +Per SEP-2640, a skill MAY also be distributed as a single packed resource (`.tar.gz` or `.zip`). Pass declarations to `registerSkillResources()`; the SDK reads each archive at startup, registers it as an MCP resource at `skill://.`, and adds an index entry whose `archives` array carries the archive's `url`, `mimeType`, and a SHA-256 `digest`: + +```typescript +registerSkillResources(server, skillMap, "./skills", { + archives: [ + { + name: "pdf-processing", + description: "Extract and assemble PDFs", + skillPath: "pdf-processing", + archivePath: "./archives/pdf-processing.tar.gz", + // format inferred from extension; pass "tar.gz" | "zip" to override + }, + ], +}); +``` + +The SEP requires that the final segment of `skillPath` equals the skill's frontmatter `name`; the SDK validates this and throws on mismatch. + +## Client usage + +### Quick start + +Discover skills and build a system prompt catalog in one call: + +```typescript +import { discoverAndBuildCatalog } from "@modelcontextprotocol/experimental-ext-skills/client"; + +const { skills, catalog } = await discoverAndBuildCatalog(client, { + serverName: "my-skills-server", +}); + +console.log(`Discovered ${skills.length} skill(s)`); +// Inject `catalog` into your agent's system prompt +``` + +`discoverAndBuildCatalog()` handles the recommended discovery strategy (try `skill://index.json` first, fall back to `resources/list`) and builds an XML catalog with behavioral instructions for the model. All options are optional: + +- Pass `serverName` when your reader tool takes a `server` parameter (e.g., the bundled `READ_RESOURCE_TOOL`); omit it for host-scoped readers that take only `uri`. The catalog drops the `with server …` clause when omitted. +- Pass `serverInEntries: true` to also inject `` inside every `` entry. Off by default because per-entry placement is host-implementation guidance from the host SKILL.md, not in SEP-2640. Empirically lifts first-call activation ~33% → ~90% for `(server, uri)` reader tools. +- Pass `instructions: true` to enable the SEP's third discovery path (mining server `instructions` for skill URIs). Off by default. + +### Step by step + +For more control, use the lower-level functions directly: + +```typescript +import { + discoverSkills, + listSkillsFromIndex, + readSkillUri, + readSkillContent, + readSkillArchive, + readSkillDocument, + buildSkillsCatalog, + buildSkillsSummary, + serverSupportsDirectoryRead, + readDirectory, + walkDirectory, + verifyDigest, + READ_RESOURCE_TOOL, +} from "@modelcontextprotocol/experimental-ext-skills/client"; + +// Discover skills (index-first with fallback, always returns an array) +// Includes both type: "skill-md" and type: "archive" entries. +const skills = await discoverSkills(client); + +// Or read skill://index.json directly (returns null if unavailable). Each +// summary carries name/description (from the entry's verbatim frontmatter), +// uri, and the index `digest`. +const indexSkills = await listSkillsFromIndex(client); + +// Enumerate a skill directory (only if the server declared the capability). +if (serverSupportsDirectoryRead(client)) { + const { resources } = await readDirectory(client, "skill://acme/billing/refunds"); + const allFiles = await walkDirectory(client, "skill://acme/billing/refunds"); +} + +// Read skill content by URI (works with any scheme: skill://, repo://, github://, etc.) +const content = await readSkillUri(client, skill.uri); + +// Integrity-check a read against the index digest (SEP-2640: hosts MUST verify). +if (skill.digest) { + const ok = verifyDigest(content, skill.digest); +} + +// Or by skill path (convenience, skill:// scheme only) +const md = await readSkillContent(client, "acme/billing/refunds"); + +// Fetch + unpack an archive-distributed skill +const archive = await readSkillArchive(client, "skill://pdf-processing.tar.gz"); +const archiveSkillMd = archive.files.get("SKILL.md")!.toString("utf-8"); + +// Read a supporting file +const doc = await readSkillDocument(client, "acme/billing/refunds", "templates/refund-email-template.md"); + +// Build catalog or summary for context injection +const catalog = buildSkillsCatalog(skills, { toolName: "read_resource", serverName: "my-server" }); +const summary = buildSkillsSummary(skills); + +// READ_RESOURCE_TOOL — tool schema for model-driven skill loading +// Hosts expose this so the model can call read_resource(server, uri) +console.log(READ_RESOURCE_TOOL); +``` + +### Reading archive-distributed skills + +`listSkillsFromIndex()` returns archive entries with `type: "archive"`. Use `readSkillArchive()` to fetch and unpack: + +```typescript +import { readSkillArchive } from "@modelcontextprotocol/experimental-ext-skills/client"; + +const skills = await listSkillsFromIndex(client) ?? []; +for (const summary of skills) { + if (summary.type === "archive") { + const archive = await readSkillArchive(client, summary.uri); + const skillMd = archive.files.get("SKILL.md")!.toString("utf-8"); + // Other files in archive.files keyed by relative path — + // identical namespace to skill:/// + } +} +``` + +The host MUST support both `.tar.gz` (`application/gzip`) and `.zip` (`application/zip`); the SDK dispatches on `mimeType` (with URL-suffix fallback). Archive safety is enforced: path traversal, absolute paths, and out-of-tree symlinks are rejected, with bounded total size, per-file size, and entry count to defend against decompression bombs. + +### Digest: integrity and caching + +Each index entry carries a `sha256:{hex}` `digest` (over the SKILL.md raw bytes; archives carry their own under `archives[].digest`). It serves **two distinct purposes**, and they are different operations: + +**1. Integrity / tamper-detection** — verify retrieved content against the advertised digest (SEP-2640 asks hosts to do this): + +```typescript +import { verifyDigest, readSkillUriVerified } from "@modelcontextprotocol/experimental-ext-skills/client"; + +const content = await readSkillContent(client, summary.skillPath); +if (summary.digest && !verifyDigest(content, summary.digest)) { + // content was altered in transit or drifted from what the index advertised +} + +// Or read + verify in one call (throws on mismatch): +const verified = await readSkillUriVerified(client, summary.uri, summary.digest!); +``` + +`SKILL.md` is UTF-8, so hashing the received `text` (as UTF-8) matches the server's raw-byte hash exactly — a UTF-8 decode→encode round-trip is byte-identical (CRLF, BOM, multibyte all preserved). Only genuinely non-UTF-8 content (disallowed for `SKILL.md`) would differ. + +**2. Caching** — this is the digest's headline purpose, and it does **not** hash content. Store each skill's digest from a prior index read; on a later poll, refetch only the (small) index and compare the new digest against the stored one. Equal ⇒ the skill content is unchanged, skip refetching it (skill payloads can be large): + +```typescript +// `cache` is your own Map from a previous run. +const fresh = await listSkillsFromIndex(client) ?? []; +for (const s of fresh) { + if (s.digest && cache.get(s.uri) === s.digest) continue; // unchanged — skip refetch + const content = await readSkillContent(client, s.skillPath); + cache.set(s.uri, s.digest!); + // ... (re)load content +} +``` + +The SDK exposes `SkillSummary.digest` for this comparison but doesn't manage a cache store — that belongs to the host. The same compare also surfaces drift (e.g. an agent edited a skill locally) for a `/skills list`-style view. + +### Scheme-agnostic discovery + +Per the SEP, `skill://` is SHOULD, not MUST. Servers may serve skills under any URI scheme (e.g., `repo://`, `github://`) provided they are listed in `skill://index.json`. The discovery functions (`discoverSkills`, `listSkillsFromIndex`) handle any scheme in index entries, and `readSkillUri()` reads any URI regardless of scheme. + +### Server `instructions` as a discovery path + +The SEP lists three discovery paths feeding the host's catalog: `skill://index.json`, server `instructions`, and direct `resources/read`. `discoverSkills()` and `discoverAndBuildCatalog()` accept `{ instructions: true }` to opt into mining `client.getInstructions()` for `://...SKILL.md` URIs and merging them with index hits (deduplicated by URI). This is **off by default** — most servers don't name skill URIs in their instructions, and turning it on costs one `resources/read` round-trip per URI mentioned. Turn it on for documentation-server / gateway / template-only servers that don't enumerate via `index.json`. + +```typescript +const skills = await discoverSkills(client, { instructions: true }); +``` + +Pass `extractor` to override the built-in regex when the server uses a non-standard URI convention in its instructions text (URIs inside code fences with custom syntax, JSON-encoded URI lists, etc.): + +```typescript +const skills = await discoverSkills(client, { + instructions: true, + extractor: (text) => JSON.parse(text)["skills"] as string[], +}); +``` + +Lower-level helpers are also exported: + +```typescript +import { + extractSkillUrisFromInstructions, + listSkillsFromInstructions, +} from "@modelcontextprotocol/experimental-ext-skills/client"; + +const uris = extractSkillUrisFromInstructions(client.getInstructions()); +const fromInstructions = await listSkillsFromInstructions( + client, + client.getInstructions() ?? "", + { extractor: myExtractor }, // optional +); +``` + +### Per-entry `` in the system-prompt catalog + +`buildSkillsCatalog(skills, { toolName, serverName, serverInEntries: true })` injects `{name}` into every `` entry. This puts the server name visibly next to each URI the model might pass to a `(server, uri)` reader tool. The host SKILL.md flags this as the way to keep first-call activation reliability ~90% (vs ~33% without). + +`serverInEntries` defaults to **false** because per-entry placement isn't in SEP-2640 — only the empirical activation guidance from the host SKILL.md. Hosts that use `(server, uri)` reader tools (like the bundled `READ_RESOURCE_TOOL`) should opt in; hosts whose readers are already scoped to one server can leave it off. The wrapper-level mention of `serverName` in the prose instructions remains independent of this flag. + +## URI scheme + +``` +skill://code-review/SKILL.md # single-segment path +skill://acme/billing/refunds/SKILL.md # multi-segment path +skill://acme/billing/refunds/templates/email.md # supporting file +skill://pdf-processing.tar.gz # archive distribution +skill://index.json # discovery index +``` + +URI utilities are available from the main import: + +```typescript +import { parseSkillUri, buildSkillUri, isSkillContentUri } from "@modelcontextprotocol/experimental-ext-skills"; +``` + +## Related + +- [Skills Extension SEP (PR #69)](https://github.com/modelcontextprotocol/experimental-ext-skills/pull/69) -- the spec this implements +- [Skills Over MCP Interest Group](https://github.com/modelcontextprotocol/experimental-ext-skills) -- parent repository +- [Agent Skills specification](https://agentskills.io/specification) -- the skill format (frontmatter, directory layout) this transports +- [Server example](../../examples/skills-server/typescript/) -- reference MCP server +- [Client example](../../examples/skills-client/typescript/) -- reference MCP client + +## License + +Apache-2.0 diff --git a/typescript/sdk/package-lock.json b/typescript/sdk/package-lock.json new file mode 100644 index 0000000..57a3f88 --- /dev/null +++ b/typescript/sdk/package-lock.json @@ -0,0 +1,3078 @@ +{ + "name": "@modelcontextprotocol/experimental-ext-skills", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@modelcontextprotocol/experimental-ext-skills", + "version": "0.1.0", + "license": "Apache-2.0", + "dependencies": { + "tar-stream": "^3.1.7", + "yaml": "^2.7.0", + "yauzl": "^3.2.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "@types/node": "^22.0.0", + "@types/tar-stream": "^3.1.3", + "@types/yauzl": "^2.10.3", + "@types/yazl": "^2.4.6", + "typescript": "^5.7.0", + "vitest": "^3.0.0", + "yazl": "^3.3.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.1.tgz", + "integrity": "sha512-xB0b51TB7IfDEzAojXahmr+gfA00uYVInJGgNNkeQG6RPnCPGr7udsylFLTubuIUSRE6FkcI1NElyRt83PP5oQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.1.tgz", + "integrity": "sha512-XOjPId0qwSDKHaIsdzHJtKCxX0+nH8MhBwvrNsT7tVyKmdTx1jJ4XzN5RZXCdTzMpufLb+B8llTC0D8uCrLhcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.1.tgz", + "integrity": "sha512-vQuRd28p0gQpPrS6kppd8IrWmFo42U8Pz1XLRjSZXq5zCqyMDYFABT7/sywL11mO1EL10Qhh7MVPEwkG8GiBeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.1.tgz", + "integrity": "sha512-x6VG6U29+Ivlnajrg1IHdzXeAwSoEHBFVO+CtC9Brugx6de712CUJobRUxsIA0KYrQvCmzNrMPFTT1A4CCqNTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.1.tgz", + "integrity": "sha512-Sgi0Uo6t1YCHJMNO3Y8+bm+SvOanUGkoZKn/VJPwYUe2kp31X5KnXmzKd/NjW8iA3gFcfNZ64zh14uOGrIllCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.1.tgz", + "integrity": "sha512-AM4xnwEZwukdhk7laMWfzWu9JGSVnJd+Fowt6Fd7QW1nrf3h0Hp7Qx5881M4aqrUlKBCybOxz0jofvIIfl7C5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.1.tgz", + "integrity": "sha512-KUizqxpwaR2AZdAUsMWfL/C94pUu7TKpoPd88c8yFVixJ+l9hejkrwoK5Zj3wiNh65UeyryKnJyxL1b7yNqFQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.1.tgz", + "integrity": "sha512-MZoQ/am77ckJtZGFAtPucgUuJWiop3m2R3lw7tC0QCcbfl4DRhQUBUkHWCkcrT3pqy5Mzv5QQgY6Dmlba6iTWg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.1.tgz", + "integrity": "sha512-Sez95TP6xGjkWB1608EfhCX1gdGrO5wzyN99VqzRtC17x/1bhw5VU1V0GfKUwbW/Xr1J8mSasoFoJa6Y7aGGSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.1.tgz", + "integrity": "sha512-9Cs2Seq98LWNOJzR89EGTZoiP8EkZ9UbQhBlDgfAkM6asVna1xJ04W2CLYWDN/RpUgOjtQvcv8wQVi1t5oQazA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.1.tgz", + "integrity": "sha512-n9yqttftgFy7IrNEnHy1bOp6B4OSe8mJDiPkT7EqlM9FnKOwUMnCK62ixW0Kd9Clw0/wgvh8+SqaDXMFvw3KqQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.1.tgz", + "integrity": "sha512-SfpNXDzVTqs/riak4xXcLpq5gIQWsqGWMhN1AGRQKB4qGSs4r0sEs3ervXPcE1O9RsQ5bm8Muz6zmQpQnPss1g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.1.tgz", + "integrity": "sha512-LjaChED0wQnjKZU+tsmGbN+9nN1XhaWUkAlSbTdhpEseCS4a15f/Q8xC2BN4GDKRzhhLZpYtJBZr2NZhR0jvNw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.1.tgz", + "integrity": "sha512-ojW7iTJSIs4pwB2xV6QXGwNyDctvXOivYllttuPbXguuKDX5vwpqYJsHc6D2LZzjDGHML414Tuj3LvVPe1CT1A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.1.tgz", + "integrity": "sha512-FP+Q6WTcxxvsr0wQczhSE+tOZvFPV8A/mUE6mhZYFW9/eea/y/XqAgRoLLMuE9Cz0hfX5bi7p116IWoB+P237A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.1.tgz", + "integrity": "sha512-L1uD9b/Ig8Z+rn1KttCJjwhN1FgjRMBKsPaBsDKkfUl7GfFq71pU4vWCnpOsGljycFEbkHWARZLf4lMYg3WOLw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.1.tgz", + "integrity": "sha512-EZc9NGTk/oSUzzOD4nYY4gIjteo2M3CiozX6t1IXGCOdgxJTlVu/7EdPeiqeHPSIrxkLhavqpBAUCfvC6vBOug==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.1.tgz", + "integrity": "sha512-NQ9KyU1Anuy59L8+HHOKM++CoUxrQWrZWXRik4BJFm+7i5NP6q/SW43xIBr80zzt+PDBJ7LeNmloQGfa0JGk0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.1.tgz", + "integrity": "sha512-GZkLk2t6naywsveSFBsEb0PLU+JC9ggVjbndsbG20VPhar6D1gkMfCx4NfP9owpovBXTN+eRdqGSkDGIxPHhmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.1.tgz", + "integrity": "sha512-1hjG9Jpl2KDOetr64iQd8AZAEjkDUUK5RbDkYWsViYLC1op1oNzdjMJeFiofcGhqbNTaY2kfgqowE7DILifsrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.1.tgz", + "integrity": "sha512-ARoKfflk0SiiYm3r1fmF73K/yB+PThmOwfWCk1sr7x/k9dc3uGLWuEE9if+Pw21el8MSpp3TMnG5vLNsJ/MMGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.1.tgz", + "integrity": "sha512-oOST61G6VM45Mz2vdzWMr1s2slI7y9LqxEV5fCoWi2MDONmMvgsJVHSXxce/I2xOSZPTZ47nDPOl1tkwKWSHcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.1.tgz", + "integrity": "sha512-x5WgLi5dWpRz7WclKBGEF15LcWTh0ewrHM6Cq4A+WUbkysUMZNeqt05bwPonOQ3ihPS/WMhAZV5zB1DfnI4Sxg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.1.tgz", + "integrity": "sha512-wS+zHAJRVP5zOL0e+a3V3E/NTEwM2HEvvNKoDy5Xcfs0o8lljxn+EAFPkUsxihBdmDq1JWzXmmB9cbssCPdxxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.1.tgz", + "integrity": "sha512-rhHyrMeLpErT/C7BxcEsU4COHQUzHyrPYW5tOZUeUhziNtRuYxmDWvqQqzpuUt8xpOgmbKa1btGXfnA/ANVO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/tar-stream": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.4.tgz", + "integrity": "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yazl": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.6.tgz", + "integrity": "sha512-/ifFjQtcKaoZOjl5NNCQRR0fAKafB3Foxd7J/WvFPTMea46zekapcR30uzkwIkKAAuq5T6d0dkwz754RFH27hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "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==", + "dev": true, + "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/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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==", + "dev": true, + "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==", + "dev": true, + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "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.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.8", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", + "integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.1.tgz", + "integrity": "sha512-iZKH8BeoCwTCBTZBZWQQMreekd4mdomwdjIQ40GC1oZm6o+8PnNMIxFOiCsGMWeS8iDJ7KZcl7KwmKk/0HOQpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.1", + "@rollup/rollup-android-arm64": "4.59.1", + "@rollup/rollup-darwin-arm64": "4.59.1", + "@rollup/rollup-darwin-x64": "4.59.1", + "@rollup/rollup-freebsd-arm64": "4.59.1", + "@rollup/rollup-freebsd-x64": "4.59.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.1", + "@rollup/rollup-linux-arm-musleabihf": "4.59.1", + "@rollup/rollup-linux-arm64-gnu": "4.59.1", + "@rollup/rollup-linux-arm64-musl": "4.59.1", + "@rollup/rollup-linux-loong64-gnu": "4.59.1", + "@rollup/rollup-linux-loong64-musl": "4.59.1", + "@rollup/rollup-linux-ppc64-gnu": "4.59.1", + "@rollup/rollup-linux-ppc64-musl": "4.59.1", + "@rollup/rollup-linux-riscv64-gnu": "4.59.1", + "@rollup/rollup-linux-riscv64-musl": "4.59.1", + "@rollup/rollup-linux-s390x-gnu": "4.59.1", + "@rollup/rollup-linux-x64-gnu": "4.59.1", + "@rollup/rollup-linux-x64-musl": "4.59.1", + "@rollup/rollup-openbsd-x64": "4.59.1", + "@rollup/rollup-openharmony-arm64": "4.59.1", + "@rollup/rollup-win32-arm64-msvc": "4.59.1", + "@rollup/rollup-win32-ia32-msvc": "4.59.1", + "@rollup/rollup-win32-x64-gnu": "4.59.1", + "@rollup/rollup-win32-x64-msvc": "4.59.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yauzl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz", + "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yazl": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-3.3.1.tgz", + "integrity": "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "^1.0.0" + } + }, + "node_modules/yazl/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "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==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json new file mode 100644 index 0000000..78778e2 --- /dev/null +++ b/typescript/sdk/package.json @@ -0,0 +1,55 @@ +{ + "name": "@olaservo/ext-skills", + "version": "0.10.0", + "description": "SDK for the Skills Extension SEP", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./server": { + "types": "./dist/server/index.d.ts", + "import": "./dist/server/index.js" + }, + "./client": { + "types": "./dist/client/index.d.ts", + "import": "./dist/client/index.js" + } + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsc", + "test": "vitest run", + "test:watch": "vitest", + "prepublishOnly": "npm run build" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "dependencies": { + "tar-stream": "^3.1.7", + "yaml": "^2.7.0", + "yauzl": "^3.2.0", + "zod": "^3.23.0" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "@types/node": "^22.0.0", + "@types/tar-stream": "^3.1.3", + "@types/yauzl": "^2.10.3", + "@types/yazl": "^2.4.6", + "typescript": "^5.7.0", + "vitest": "^3.0.0", + "yazl": "^3.3.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "license": "Apache-2.0" +} diff --git a/typescript/sdk/src/_client.ts b/typescript/sdk/src/_client.ts new file mode 100644 index 0000000..569416c --- /dev/null +++ b/typescript/sdk/src/_client.ts @@ -0,0 +1,901 @@ +/** + * Client-side utilities for discovering, reading, and summarizing skills + * exposed as MCP resources by a skills server. + * + * Each MCP Client instance is inherently server-scoped — it represents a + * connection to a single MCP server. This is the architectural basis for + * excluding server names from skill:// URIs: disambiguation happens at + * the call site, not in the URI. + * + * Per the SEP, skill:// is SHOULD, not MUST. Servers MAY serve skills + * under any scheme (e.g., github://, repo://) provided each skill is + * listed in skill://index.json. The index is the authoritative record + * of which resources are skills; outside the index, hosts recognize + * skills by the skill:// scheme prefix. + * + * Key evolution from previous version: + * - Multi-segment skill paths: skillPath may have a prefix before name + * (per the SEP, the final segment of skillPath equals frontmatter name) + * - SDK wrappers per the SEP: listSkills(), listSkillsFromIndex(), readSkillUri() + */ + +import { createHash } from "node:crypto"; +import { parse as parseYaml } from "yaml"; +import type { + SkillSummary, + SkillIndex, + SkillsCatalogOptions, + DiscoverSkillsOptions, + DiscoverCatalogOptions, + DiscoverCatalogResult, + InstructionsUriExtractor, + UnpackedSkillArchive, + ExtractArchiveOptions, +} from "./types.js"; +import { generateSkillsXMLFromSummaries } from "./xml.js"; +import { + buildSkillUri, + INDEX_JSON_URI, + parseSkillUri, + SKILL_FILENAME, + extractSkillPathFromUri, +} from "./uri.js"; +import { + extractSkillArchive, + stripArchiveSuffix, + detectArchiveFormat, +} from "./archive.js"; +import { + DIRECTORY_READ_METHOD, + DirectoryReadResultSchema, + type DirectoryChild, + type DirectoryReadResult, +} from "./directory.js"; +import { SKILLS_EXTENSION_ID } from "./resource-extensions.js"; + +/** + * Minimal structural interface for an MCP Client. + * Using a structural type avoids issues with duplicate SDK installations + * causing private-property type incompatibilities. + */ +export interface SkillsClient { + listResources( + params?: { cursor?: string }, + ): Promise<{ + resources: Array<{ + uri: string; + name?: string; + description?: string; + mimeType?: string; + }>; + nextCursor?: string; + }>; + readResource(params: { + uri: string; + }): Promise<{ + contents: Array<{ + uri?: string; + mimeType?: string; + text?: string; + blob?: string; + }>; + }>; + /** + * Optional. Returns the connected server's `instructions` string from the + * `initialize` response, when the underlying client exposes it. Used by + * `discoverSkills()` to mine instructions for skill URIs per the SEP's + * third discovery path. + */ + getInstructions?(): string | undefined; + /** + * Optional. The connected server's advertised capabilities (the MCP SDK + * Client exposes this). Used to gate `resources/directory/read` on the + * server having declared `extensions["io.modelcontextprotocol/skills"] + * .directoryRead`. + */ + getServerCapabilities?(): { + extensions?: Record; + } | undefined; + /** + * Optional. Low-level JSON-RPC request, used for extension methods that + * have no high-level wrapper (e.g. `resources/directory/read`). Mirrors the + * MCP SDK Client's `request(request, resultSchema, options?)`. The result + * schema is passed through to the underlying client; we annotate the return + * as `unknown` and narrow at the call site. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + request?(request: { method: string; params?: unknown }, resultSchema: any): Promise; +} + +/** + * MCP Tool definition type — matches the SDK's Tool interface. + */ +export interface ToolDefinition { + name: string; + description: string; + inputSchema: Record; + annotations?: Record; +} + +/** + * MCP Tool definition for a generic read_resource tool. + * + * The model calls read_resource(server, uri) and the host routes + * to the correct MCP Client instance based on the server name. + * This tool is general-purpose — it reads any MCP resource — and + * benefits resource use cases beyond skills. + * + * Per the SEP: "Including the server name disambiguates identical + * skill:// URIs served by different connected servers." + */ +export const READ_RESOURCE_TOOL: ToolDefinition = { + name: "read_resource", + description: "Read an MCP resource from a connected server.", + inputSchema: { + type: "object", + properties: { + server: { + type: "string", + description: "Name of the connected MCP server", + }, + uri: { + type: "string", + description: + "The resource URI, e.g. skill://git-workflow/SKILL.md", + }, + }, + required: ["server", "uri"], + }, + annotations: { + readOnlyHint: true, + idempotentHint: true, + openWorldHint: false, + }, +}; + +/** + * MCP Tool definition for a name-keyed read_skill tool. + * + * The model calls read_skill(name) and the host looks the name up in + * its skill registry, routing to a filesystem read or an MCP + * `resources/read` based on origin. The model neither knows nor cares + * which path was taken — this matches the SEP's "Hosts: End-to-End + * Integration" guidance for hosts that already expose a name-keyed + * skill loader for filesystem skills and want to extend it to cover + * MCP-served skills. + * + * Companion to READ_RESOURCE_TOOL: the latter is general-purpose and + * disambiguates by `(server, uri)`; this one is skills-specific and + * disambiguates by host registry lookup. + */ +export const READ_SKILL_TOOL: ToolDefinition = { + name: "read_skill", + description: "Load a skill's SKILL.md into context.", + inputSchema: { + type: "object", + properties: { + name: { + type: "string", + description: "The skill name", + }, + }, + required: ["name"], + }, + annotations: { + readOnlyHint: true, + idempotentHint: true, + openWorldHint: false, + }, +}; + +/** + * List all skills available from an MCP client via resources/list. + * + * Calls resources/list, filters for skill://{skillPath}/SKILL.md URIs, + * and returns SkillSummary objects with both name and skillPath. + * Handles pagination automatically. + * + * This function only finds skills using the skill:// scheme. Per the SEP, + * "outside the index, hosts recognize skills by the skill:// scheme prefix." + * For servers that use other schemes, use listSkillsFromIndex() instead — + * the index is the authoritative record of which resources are skills. + * + * Per PR #69: this is the SDK wrapper for client.list_skills(). + */ +export async function listSkills(client: SkillsClient): Promise { + const skills: SkillSummary[] = []; + let cursor: string | undefined; + + do { + const result = await client.listResources( + cursor ? { cursor } : undefined, + ); + + for (const resource of result.resources) { + const parsed = parseSkillUri(resource.uri); + if (!parsed) continue; + if ( + parsed.filePath !== SKILL_FILENAME && + parsed.filePath.toLowerCase() !== "skill.md" + ) + continue; + + skills.push({ + name: resource.name ?? parsed.skillPath, + skillPath: parsed.skillPath, + uri: resource.uri, + description: resource.description, + mimeType: resource.mimeType, + }); + } + + cursor = result.nextCursor; + } while (cursor); + + return skills; +} + +/** + * Fetch and parse skill://index.json from an MCP server. + * Returns the parsed SkillIndex or null if unavailable. + */ +async function fetchAndParseIndex( + client: SkillsClient, +): Promise { + try { + const result = await client.readResource({ uri: INDEX_JSON_URI }); + const content = result.contents[0]; + if (!content || !("text" in content) || !content.text) return null; + + const index = JSON.parse(content.text) as SkillIndex; + + // Per SEP-2640 the index carries no `$schema`/version marker — the format + // is versioned by the extension itself, so there is nothing to validate + // beyond the presence of a `skills` array. + if (!index.skills || !Array.isArray(index.skills)) return null; + + return index; + } catch { + return null; + } +} + +/** Pull a string field from a frontmatter object, or undefined. */ +function frontmatterString( + frontmatter: Record | undefined, + key: string, +): string | undefined { + const v = frontmatter?.[key]; + return typeof v === "string" ? v : undefined; +} + +/** + * List skills by reading the well-known skill://index.json resource. + * + * This is the SEP's primary enumeration mechanism, following the Agent Skills + * well-known URI discovery index format. Returns null if the server does not + * expose skill://index.json (enumeration is optional per the SEP). + * + * Scheme-agnostic: index entries may use any URI scheme (skill://, github://, + * repo://, etc.) per the SEP. For skill:// URIs, skillPath is extracted from + * the URI structure. For other schemes, skillPath falls back to entry.name + * (the skill's frontmatter name). The uri field always carries the raw URL + * from the index, regardless of scheme. + * + * Hosts MUST NOT treat an absent or empty index as proof that a server has + * no skills — a skill:// URI is always directly readable via resources/read. + * + * Per SEP-2640 each entry is type-less: a `url` (with `digest`) means the + * skill is served as individual files; a non-empty `archives` array means it + * is available packed. Name and description come from the entry's verbatim + * `frontmatter`. An entry with neither `url` nor `archives` is malformed and + * is skipped. + */ +export async function listSkillsFromIndex( + client: SkillsClient, +): Promise { + const index = await fetchAndParseIndex(client); + if (!index) return null; + + const summaries: SkillSummary[] = []; + for (const entry of index.skills) { + const name = frontmatterString(entry.frontmatter, "name"); + const description = frontmatterString(entry.frontmatter, "description"); + + if (entry.url) { + // Per SEP-2640, `` structural constraints apply regardless + // of scheme. Extract the path between `://` and `/SKILL.md`; + // fall back to the frontmatter name only when the URL lacks that form. + const skillPath = extractSkillPathFromUri(entry.url) ?? name ?? entry.url; + summaries.push({ + name: name ?? skillPath, + skillPath, + uri: entry.url, + type: "skill-md", + description, + mimeType: "text/markdown", + digest: entry.digest, + archives: entry.archives, + }); + } else if (entry.archives && entry.archives.length > 0) { + // Archive-only skill. Expose the first archive's URL on `uri` so callers + // know how to fetch; the post-unpack `skillPath` is derived from it by + // stripping the archive suffix (skill://pdf-processing.tar.gz unpacks to + // skill://pdf-processing/). + const primary = entry.archives[0]; + const stripped = stripArchiveSuffix(primary.url); + const skillPath = + extractSkillPathFromUri(stripped + "/SKILL.md") ?? name ?? primary.url; + summaries.push({ + name: name ?? skillPath, + skillPath, + uri: primary.url, + type: "archive", + description, + mimeType: + primary.mimeType ?? + (detectArchiveFormat(undefined, primary.url) === "zip" + ? "application/zip" + : "application/gzip"), + digest: primary.digest, + archives: entry.archives, + }); + } + // Entries with neither `url` nor `archives` violate SEP-2640 — skip. + } + return summaries; +} + +/** + * Read a resource by its full URI from an MCP server. + * + * Scheme-agnostic: works with any URI scheme (skill://, github://, repo://, etc.). + * This is the primary read function for skills discovered via listSkillsFromIndex(), + * which may return URIs in any scheme. Pass the SkillSummary.uri value directly. + * + * Per PR #69: this is the SDK wrapper for client.read_skill_uri(). + */ +export async function readSkillUri( + client: SkillsClient, + uri: string, +): Promise { + const result = await client.readResource({ uri }); + const content = result.contents[0]; + if (content && "text" in content && typeof content.text === "string") { + return content.text; + } + throw new Error(`Expected text content for ${uri}`); +} + +/** + * Verify that `data` matches an expected `sha256:{hex}` digest from a + * `skill://index.json` entry — the integrity/tamper check SEP-2640 asks hosts + * to perform on retrieved content. + * + * Index digests are over the SKILL.md file's **raw bytes**. When `data` is a + * string (the usual case — `resources/read` returns `text`), it is hashed as + * UTF-8. This is exact for `SKILL.md`, which the Agent Skills spec requires to + * be UTF-8: a UTF-8 decode→encode round-trip is byte-identical (CRLF, BOM, and + * multibyte content all preserved), so a faithfully-served file always + * matches. Only genuinely non-UTF-8 bytes (disallowed for `SKILL.md`) would + * differ — pass a `Buffer` of the exact bytes received in that case. + * + * Note this is the *tamper-detection* use of the digest. The digest's other + * purpose — caching — is a different operation: compare the new index + * `digest` string against a previously-stored one (no content hashing). See + * the README "Caching with the index digest" section. + * + * The comparison is case-insensitive on the hex. + * + * Throws if `expected` is not a well-formed `sha256:{64 hex}` digest, so a + * caller can distinguish "content was tampered" (returns `false`) from "the + * index handed me a digest I can't interpret" (throws) — the latter must not + * be silently treated as a mismatch when SEP-2640 makes verification a MUST. + */ +export function verifyDigest( + data: Buffer | string, + expected: string, +): boolean { + if (!/^sha256:[0-9a-f]{64}$/i.test(expected)) { + throw new Error( + `Malformed digest "${expected}": expected "sha256:" followed by 64 hex characters`, + ); + } + const actual = "sha256:" + createHash("sha256").update(data).digest("hex"); + return actual.toLowerCase() === expected.toLowerCase(); +} + +/** + * Read a skill resource and verify it against an expected `sha256:{hex}` + * digest (e.g. `SkillSummary.digest`). Throws if the digest does not match. + * Returns the text content on success. + */ +export async function readSkillUriVerified( + client: SkillsClient, + uri: string, + expectedDigest: string, +): Promise { + const text = await readSkillUri(client, uri); + if (!verifyDigest(text, expectedDigest)) { + throw new Error( + `Digest mismatch for ${uri}: content does not match index digest ${expectedDigest}`, + ); + } + return text; +} + +/** + * Whether the connected server has declared the SEP-2640 `directoryRead` + * capability under `extensions["io.modelcontextprotocol/skills"]`. Clients + * MUST NOT call `resources/directory/read` unless this is true. + */ +export function serverSupportsDirectoryRead(client: SkillsClient): boolean { + const ext = client.getServerCapabilities?.()?.extensions?.[SKILLS_EXTENSION_ID]; + return !!ext && ext.directoryRead === true; +} + +/** + * List the direct children of a directory resource via the SEP-2640 + * `resources/directory/read` method. Returns the children (files and + * subdirectories — subdirectories carry `mimeType: "inode/directory"`) plus + * an optional `nextCursor` for pagination. The listing is metadata-only and + * non-recursive; descend by calling again on a child directory's URI. + * + * Throws if the server has not declared the `directoryRead` capability, or if + * the structural client does not expose a low-level `request` method. + */ +export async function readDirectory( + client: SkillsClient, + uri: string, + options?: { cursor?: string }, +): Promise { + if (!serverSupportsDirectoryRead(client)) { + throw new Error( + `Server did not declare the "directoryRead" capability; resources/directory/read is not available.`, + ); + } + if (!client.request) { + throw new Error( + `Client does not expose a low-level request() method for resources/directory/read.`, + ); + } + const result = (await client.request( + { + method: DIRECTORY_READ_METHOD, + params: { uri, ...(options?.cursor ? { cursor: options.cursor } : {}) }, + }, + DirectoryReadResultSchema, + )) as DirectoryReadResult; + return result; +} + +/** + * Walk a directory subtree breadth-first via repeated `resources/directory/read` + * calls, yielding every descendant file (not directories). Convenience over + * {@link readDirectory} for hosts that want to materialize a whole skill. + */ +export async function walkDirectory( + client: SkillsClient, + rootUri: string, +): Promise { + const files: DirectoryChild[] = []; + const queue: string[] = [rootUri]; + // Guard against a misbehaving server: don't re-enter a directory we've + // already walked (cyclic listings), and bail out of a page loop whose + // `nextCursor` never advances (which would otherwise spin forever). + const visited = new Set(); + while (queue.length > 0) { + const dir = queue.shift()!; + if (visited.has(dir)) continue; + visited.add(dir); + let cursor: string | undefined; + const seenCursors = new Set(); + do { + const { resources, nextCursor } = await readDirectory(client, dir, { + cursor, + }); + for (const child of resources) { + if (child.mimeType === "inode/directory") { + queue.push(child.uri); + } else { + files.push(child); + } + } + if (nextCursor !== undefined && seenCursors.has(nextCursor)) { + throw new Error( + `Pagination did not advance for ${dir}: server returned a repeated cursor`, + ); + } + if (nextCursor !== undefined) seenCursors.add(nextCursor); + cursor = nextCursor; + } while (cursor); + } + return files; +} + +/** + * Read a skill's SKILL.md content by skill path. + * + * Convenience method that builds a skill:// URI from the skill path. + * Only works for skills using the skill:// scheme. For other schemes, + * use readSkillUri() with the full URI from SkillSummary.uri. + */ +export async function readSkillContent( + client: SkillsClient, + skillPath: string, +): Promise { + const uri = buildSkillUri(skillPath); + return readSkillUri(client, uri); +} + +/** + * Parse name and description from SKILL.md YAML frontmatter content. + * + * Uses the `yaml` package so multi-line scalars, quoted strings, and other + * non-trivial YAML constructs are handled correctly. Returns null if the + * content lacks closed `---` frontmatter, the frontmatter is not a YAML + * mapping, or the `name` field is missing/non-string. + */ +export function parseSkillFrontmatter( + content: string, +): { name: string; description: string } | null { + if (!content.startsWith("---")) return null; + + // Match an opening `---` line followed by a closing `---` line. Using a + // line-anchored split keeps `---` inside the body (e.g., a horizontal + // rule) from terminating the frontmatter early. + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/); + if (!match) return null; + + let frontmatter: unknown; + try { + frontmatter = parseYaml(match[1]); + } catch { + return null; + } + + if (typeof frontmatter !== "object" || frontmatter === null) return null; + const fm = frontmatter as Record; + + if (typeof fm.name !== "string") return null; + const name = fm.name.trim(); + if (!name) return null; + + const description = typeof fm.description === "string" + ? fm.description.trim() + : ""; + + return { name, description }; +} + +/** + * Build a plain-text summary of available skills for context injection. + * Shows both name (identity) and skillPath (locator). + */ +export function buildSkillsSummary(skills: SkillSummary[]): string { + if (skills.length === 0) return "No skills available."; + + const lines = ["Available skills:"]; + for (const skill of skills) { + const desc = skill.description ? `: ${skill.description}` : ""; + const pathInfo = + skill.name !== skill.skillPath + ? ` [path: ${skill.skillPath}]` + : ""; + lines.push(`- ${skill.name}${pathInfo} (${skill.uri})${desc}`); + } + return lines.join("\n"); +} + +/** + * Build a structured skill catalog for system prompt injection. + * + * Produces an XML `` block (per agentskills.io guide) with + * behavioral instructions that tell the model which tool (and optionally + * which server) to use for loading skill content on demand. + * + * When the reader tool accepts a `server` parameter (e.g. the bundled + * `READ_RESOURCE_TOOL`, or Claude Code's `ReadMcpResourceTool`), pass + * `serverName` so the instructions name it. The e2e agent demo found that + * including the server name raises activation reliability from ~33% to ~90% + * for those tools — without it the model hallucinates a server name or + * skips the tool call. When the reader tool is already scoped to one + * server and only takes `uri`, omit `serverName`: the catalog will drop + * the server clause instead of telling the model about an unused argument. + * + * Scheme-agnostic: uses SkillSummary.uri as-is, so skills served under any + * URI scheme (skill://, repo://, github://, etc.) are included correctly. + * + * @returns A string ready for system prompt injection, or empty string if no skills. + */ +export function buildSkillsCatalog( + skills: SkillSummary[], + options: SkillsCatalogOptions, +): string { + if (skills.length === 0) return ""; + + const { toolName, serverName, serverInEntries } = options; + const xml = generateSkillsXMLFromSummaries(skills, { + serverName, + serverInEntries, + }); + + const instructions = serverName + ? [ + `When a task matches a skill's description, use the \`${toolName}\` tool`, + `with server \`${serverName}\` and the skill's URI to load its full`, + "instructions before proceeding.", + ] + : [ + `When a task matches a skill's description, use the \`${toolName}\` tool`, + "with the skill's URI to load its full instructions before proceeding.", + ]; + + return [ + "", + "## Available Skills", + "", + "The following skills provide specialized instructions for specific tasks.", + ...instructions, + "", + xml, + "", + ].join("\n"); +} + +/** + * Fetch a skill archive from an MCP server and unpack it in memory. + * + * Per SEP-2640, archive entries in `skill://index.json` reference a single + * resource that contains a packed skill directory (`.tar.gz` or `.zip`). + * This fetches the archive via `resources/read`, dispatches on the + * resource's `mimeType` (falling back to URL suffix), and unpacks with + * archive safety: rejects path-traversal, absolute paths, symlinks + * resolving outside the skill directory, and decompression bombs. + * + * The returned `files` map is keyed by paths relative to the skill root. + * After unpacking, `files.get("SKILL.md")` is the skill's content, and + * other entries correspond to `skill:///` exactly + * as if served as individual resources. + * + * @example + * ```typescript + * const summary = (await listSkillsFromIndex(client))! + * .find((s) => s.type === "archive")!; + * const archive = await readSkillArchive(client, summary.uri); + * const skillMd = archive.files.get("SKILL.md")!.toString("utf-8"); + * ``` + */ +export async function readSkillArchive( + client: SkillsClient, + archiveUri: string, + options?: ExtractArchiveOptions, +): Promise { + const result = await client.readResource({ uri: archiveUri }); + const content = result.contents[0]; + if (!content) { + throw new Error(`No content returned for archive ${archiveUri}`); + } + + let bytes: Buffer; + if ("blob" in content && content.blob) { + bytes = Buffer.from(content.blob, "base64"); + } else if ("text" in content && content.text !== undefined) { + // Fallback: some servers may serve archives as base64 text. The + // resources/read content shape is either text or blob; we don't + // expect text for archives but accept it as a courtesy. + bytes = Buffer.from(content.text, "base64"); + } else { + throw new Error( + `Archive resource ${archiveUri} returned neither blob nor text content`, + ); + } + + return extractSkillArchive( + bytes, + { mimeType: content.mimeType, url: archiveUri }, + options, + ); +} + +/** + * Read a supporting file from a skill directory. + * + * The documentPath is relative to the skill root (e.g., "references/REFERENCE.md"). + * Constructs a skill:// URI — only works for skills using the skill:// scheme. + */ +export async function readSkillDocument( + client: SkillsClient, + skillPath: string, + documentPath: string, +): Promise<{ text?: string; blob?: string; mimeType?: string }> { + const uri = buildSkillUri(skillPath, documentPath); + const result = await client.readResource({ uri }); + const content = result.contents[0]; + if (!content) throw new Error(`No content returned for ${uri}`); + return { + text: "text" in content ? content.text : undefined, + blob: "blob" in content ? content.blob : undefined, + mimeType: content.mimeType, + }; +} + +/** + * Extract skill URIs from a server's `instructions` string. + * + * Looks for any URI of the form `://...` mentioned in the + * instructions text, where the URI's path ends with `SKILL.md` (case + * insensitive). The host SKILL.md treats server `instructions` as one of + * the three SEP discovery paths: a server can name specific skill URIs + * that become readable without any catalog round trip. + * + * Returns a deduplicated array of URI strings, in first-seen order. + */ +export function extractSkillUrisFromInstructions( + instructions: string | undefined, +): string[] { + if (!instructions) return []; + const seen = new Set(); + const out: string[] = []; + // Match any :// token where the path ends at SKILL.md. + // Stops at whitespace and common URI-terminating characters in prose. + const regex = /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s`'"<>)\]]*?[Ss][Kk][Ii][Ll][Ll]\.[Mm][Dd]/g; + for (const match of instructions.matchAll(regex)) { + const uri = match[0]; + if (!seen.has(uri)) { + seen.add(uri); + out.push(uri); + } + } + return out; +} + +/** + * Read each URI mentioned in the server's instructions, parse the + * resulting SKILL.md frontmatter, and produce SkillSummary entries. + * + * URIs whose `resources/read` fails or whose content lacks valid + * frontmatter are silently dropped — instructions are advisory, and a + * misnamed URI shouldn't fail discovery for the rest. + * + * Pass `options.extractor` to replace the built-in regex with a custom + * URI extractor (useful for servers with non-standard URI conventions + * in their instructions text). + */ +export async function listSkillsFromInstructions( + client: SkillsClient, + instructions: string, + options?: { extractor?: InstructionsUriExtractor }, +): Promise { + const extract = options?.extractor ?? extractSkillUrisFromInstructions; + const uris = extract(instructions); + if (uris.length === 0) return []; + + // The per-URI reads are independent, so issue them concurrently rather than + // serially round-tripping. URIs we can't read or parse are dropped — they're + // advisory and shouldn't fail discovery for the rest. + const results = await Promise.allSettled( + uris.map(async (uri): Promise => { + const text = await readSkillUri(client, uri); + const fm = parseSkillFrontmatter(text); + const skillPath = extractSkillPathFromUri(uri) ?? fm?.name ?? uri; + return { + name: fm?.name ?? skillPath, + skillPath, + uri, + type: "skill-md", + description: fm?.description, + mimeType: "text/markdown", + }; + }), + ); + + return results + .filter( + (r): r is PromiseFulfilledResult => + r.status === "fulfilled", + ) + .map((r) => r.value); +} + +/** + * Merge two SkillSummary arrays, dropping the latter's entries whose URI + * already appears in the former. Preserves the first-array order. + */ +function mergeUniqueByUri( + primary: SkillSummary[], + extra: SkillSummary[], +): SkillSummary[] { + if (extra.length === 0) return primary; + const seen = new Set(primary.map((s) => s.uri)); + const merged = [...primary]; + for (const s of extra) { + if (!seen.has(s.uri)) { + merged.push(s); + seen.add(s.uri); + } + } + return merged; +} + +/** + * Discover all available skills from an MCP server. + * + * By default, follows two of the SEP's three discovery paths: + * 1. `skill://index.json` (authoritative, scheme-agnostic) + * 2. `resources/list` fallback (skill:// scheme only) + * + * Pass `{ instructions: true }` to enable the SEP's third path — mining + * the server's `instructions` string for skill URIs. When enabled, URIs + * named in `instructions` are merged with index entries (deduplicated by + * URI), so an enumerable server gets its full catalog plus any URIs the + * instructions explicitly call out, and an unenumerable server (no + * index) still surfaces what its instructions name. The fallback to + * `resources/list` runs only when both prior paths are empty. + * + * `instructions` are read via `client.getInstructions()` when the client + * exposes it (the MCP SDK Client does); structural clients without that + * method skip the second path silently. + * + * Pass `{ extractor }` to override the built-in regex used to find URIs + * inside the instructions text — useful for servers with non-standard + * URI conventions in prose. + */ +export async function discoverSkills( + client: SkillsClient, + options?: DiscoverSkillsOptions, +): Promise { + const wantInstructions = options?.instructions ?? false; + const instructions = wantInstructions ? client.getInstructions?.() : undefined; + const fromInstructions = instructions + ? await listSkillsFromInstructions(client, instructions, { + extractor: options?.extractor, + }) + : []; + + // Primary: skill://index.json (authoritative, scheme-agnostic) + const indexSkills = await listSkillsFromIndex(client); + if (indexSkills !== null && indexSkills.length > 0) { + return mergeUniqueByUri(indexSkills, fromInstructions); + } + + // No usable index — instructions next, then resources/list + if (fromInstructions.length > 0) return fromInstructions; + return listSkills(client); +} + +/** + * Discover skills and build a system prompt catalog in one call. + * + * Combines discoverSkills() and buildSkillsCatalog() — the most common + * client-side workflow. Returns both the discovered skills (for logging, + * filtering, or other use) and the ready-to-inject catalog text. + * + * The catalog includes behavioral instructions that tell the model which + * tool and server to use for loading skill content on demand. Including + * the server name raises activation reliability from ~33% to ~90%. + * + * @example + * ```typescript + * const { skills, catalog } = await discoverAndBuildCatalog(client, { + * serverName: "my-skills-server", + * }); + * // Inject `catalog` into your agent's system prompt + * ``` + */ +export async function discoverAndBuildCatalog( + client: SkillsClient, + options?: DiscoverCatalogOptions, +): Promise { + const skills = await discoverSkills(client, { + instructions: options?.instructions, + extractor: options?.extractor, + }); + const catalog = buildSkillsCatalog(skills, { + toolName: options?.toolName ?? READ_RESOURCE_TOOL.name, + serverName: options?.serverName, + serverInEntries: options?.serverInEntries, + }); + return { skills, catalog }; +} + diff --git a/typescript/sdk/src/_server.ts b/typescript/sdk/src/_server.ts new file mode 100644 index 0000000..715923a --- /dev/null +++ b/typescript/sdk/src/_server.ts @@ -0,0 +1,872 @@ +/** + * Server-side skill discovery, content loading, and MCP resource registration. + * + * Discovers Agent Skills by recursively scanning a directory for SKILL.md + * files at any depth, parses YAML frontmatter for metadata, scans for + * supplementary documents, and provides secure content loading. + * + * Multi-segment skill paths are supported (path ≠ name) per SEP-2640; + * the no-nesting constraint (a SKILL.md cannot be an ancestor of another) + * is enforced at discovery time. + */ + +import * as fs from "node:fs"; +import * as path from "node:path"; +import { createHash } from "node:crypto"; +import { parse as parseYaml } from "yaml"; +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { + SkillMetadata, + SkillDocument, + SkillIndex, + SkillIndexEntry, + SkillArchiveDeclaration, + ArchiveFormat, + RegisterSkillResourcesOptions, +} from "./types.js"; +import { getMimeType, isTextMimeType } from "./mime.js"; +import { + buildSkillUri, + INDEX_JSON_URI, + SKILL_URI_SCHEME, + isValidSkillName, +} from "./uri.js"; +import { archiveMimeType, archiveSuffix } from "./archive.js"; +import { + DirectoryReadRequestSchema, + makeDirectoryReadHandler, +} from "./directory.js"; +import { SKILLS_EXTENSION_ID } from "./resource-extensions.js"; + +/** Maximum file size for skill files (1MB). */ +const MAX_FILE_SIZE = 1 * 1024 * 1024; + +/** + * Compute a SHA-256 digest of raw bytes, formatted `sha256:{hex}` (64 + * lowercase hex), as required for `skill://index.json` entries by SEP-2640. + */ +export function sha256Digest(data: Buffer | string): string { + return "sha256:" + createHash("sha256").update(data).digest("hex"); +} + +/** + * Parse YAML frontmatter from SKILL.md content. + * Expects content to start with --- and have a closing --- on its own line. + * + * Uses a line-anchored match so a `---` inside the body (e.g. a markdown + * horizontal rule, or `---` within a multi-line YAML value) doesn't terminate + * the frontmatter early. This mirrors the client-side parseSkillFrontmatter() + * so the server and client agree on exactly where the frontmatter ends. + */ +function parseFrontmatter(content: string): { + frontmatter: Record; + body: string; +} { + if (!content.startsWith("---")) { + throw new Error("SKILL.md must start with YAML frontmatter (---)"); + } + + const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!match) { + throw new Error("SKILL.md frontmatter not properly closed with ---"); + } + + const frontmatter = parseYaml(match[1]) as Record; + if (typeof frontmatter !== "object" || frontmatter === null) { + throw new Error("SKILL.md frontmatter must be a YAML mapping"); + } + + const body = content.slice(match[0].length).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()) { + documents.push(...scanDir(fullPath, relativeTo, baseDir)); + } + } + + return documents; +} + +/** + * Scan a skill directory for all supplementary files. + * Finds all files in the skill directory (including root-level files + * and subdirectories), excluding SKILL.md / 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; + } + + const skipFiles = new Set(["SKILL.md", "skill.md"]); + + for (const entry of entries) { + const fullPath = path.join(skillDir, entry.name); + + if (entry.isDirectory()) { + documents.push(...scanDir(fullPath, skillDir, baseDir)); + } else if (entry.isFile() && !skipFiles.has(entry.name)) { + if (!isPathWithinBase(fullPath, baseDir)) continue; + + try { + const stat = fs.statSync(fullPath); + if (stat.size > MAX_FILE_SIZE) continue; + + const relativePath = path + .relative(skillDir, fullPath) + .replace(/\\/g, "/"); + documents.push({ + path: relativePath, + mimeType: getMimeType(entry.name), + size: stat.size, + }); + } catch { + // Skip files we can't stat + } + } + } + + return documents; +} + +/** + * Recursively find all SKILL.md files under a directory. + * Returns an array of { skillMdPath, skillDir, skillPath } objects. + * + * The `skillPath` is the relative directory path from skillsDir to the + * directory containing SKILL.md, using forward slashes. This becomes the + * multi-segment URI locator. + * + * Enforces the no-nesting constraint: a SKILL.md cannot be an ancestor + * directory of another SKILL.md. + */ +function findSkillFiles( + dir: string, + skillsDir: string, + ancestorHasSkill: boolean, +): Array<{ skillMdPath: string; skillDir: string; skillPath: string }> { + const results: Array<{ + skillMdPath: string; + skillDir: string; + skillPath: string; + }> = []; + + if (!fs.existsSync(dir)) return results; + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + return results; + } + + // Check if this directory contains a SKILL.md + let skillMdPath: string | null = null; + for (const name of ["SKILL.md", "skill.md"]) { + const candidate = path.join(dir, name); + if (fs.existsSync(candidate)) { + skillMdPath = candidate; + break; + } + } + + const hasSkill = skillMdPath !== null; + + if (hasSkill && ancestorHasSkill) { + // No-nesting constraint: skip this SKILL.md (ancestor already has one) + console.error( + `[skills] Skipping nested SKILL.md at ${skillMdPath} — ancestor directory already contains a skill`, + ); + } else if (hasSkill && skillMdPath) { + const skillPath = path.relative(skillsDir, dir).replace(/\\/g, "/") || "."; + results.push({ + skillMdPath, + skillDir: dir, + skillPath: skillPath === "." ? path.basename(dir) : skillPath, + }); + } + + // Recurse into subdirectories + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const subdir = path.join(dir, entry.name); + if (!isPathWithinBase(subdir, skillsDir)) continue; + results.push(...findSkillFiles(subdir, skillsDir, ancestorHasSkill || hasSkill)); + } + + return results; +} + +/** + * Discover all skills in a directory tree. + * + * Recursively scans for SKILL.md files at any depth (not just immediate + * subdirectories). The relative directory path from skillsDir becomes the + * multi-segment `skillPath` used in skill:// URIs. + * + * Returns a Map keyed by skillPath (not name), since the path is the + * unique locator within a server. + * + * Security: Skips files larger than MAX_FILE_SIZE, validates frontmatter, + * enforces path containment and no-nesting constraint. + */ +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 skillFiles = findSkillFiles(resolvedDir, resolvedDir, false); + + for (const { skillMdPath, skillDir, skillPath } of skillFiles) { + // 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 { + // Read raw bytes once: the digest is over the raw file bytes (SEP-2640), + // while parsing needs the UTF-8 decoding. + const fileBytes = fs.readFileSync(skillMdPath); + const content = fileBytes.toString("utf-8"); + const { frontmatter } = parseFrontmatter(content); + const digest = sha256Digest(fileBytes); + + 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; + } + + // SEP constraint: final segment of skillPath MUST equal frontmatter name + const finalSegment = skillPath.split("/").pop()!; + const trimmedName = name.trim(); + if (finalSegment !== trimmedName) { + console.error( + `Skill at ${skillDir}: frontmatter name "${trimmedName}" does not match final path segment "${finalSegment}". ` + + `Per the SEP, the final segment of the skill path must equal the frontmatter name.`, + ); + continue; + } + + // SEP constraint: the final segment (= frontmatter name) MUST satisfy + // the Agent Skills naming rule (lowercase letters, digits, hyphens). + if (!isValidSkillName(trimmedName)) { + console.error( + `Skill at ${skillDir}: name "${trimmedName}" violates the Agent Skills naming rule. ` + + `Names must contain only lowercase letters, digits, and hyphens.`, + ); + continue; + } + + if (skillMap.has(skillPath)) { + console.error( + `Warning: Duplicate skill path "${skillPath}" at ${skillMdPath} — keeping first`, + ); + continue; + } + + // Scan for supplementary documents + const documents = scanDocuments(skillDir, resolvedDir); + + skillMap.set(skillPath, { + name: name.trim(), + skillPath, + description: description.trim(), + frontmatter, + digest, + absolutePath: skillMdPath, + skillDir, + documents, + size: stat.size, + lastModified: stat.mtime.toISOString(), + }); + } 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 { + if (!skillPath.endsWith(".md")) { + throw new Error("Only .md files can be read"); + } + + if (!isPathWithinBase(skillPath, skillsDir)) { + throw new Error("Path escapes the skills directory"); + } + + 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. + * Returns text content for text MIME types and base64-encoded content for binary. + * + * Security: Validates path within skills directory, rejects path traversal, + * enforces file size limit. + */ +export function loadDocument( + skill: SkillMetadata, + documentPath: string, + skillsDir: string, + isText: boolean, +): { text: string } | { blob: string } { + // Reject `..` as a path *segment* (traversal), not as a substring — a + // filename like `notes..final.md` is legitimate. `isPathWithinBase` below is + // the real containment guard; this matches archive.ts:validateEntryPath. + if (documentPath.split(/[\\/]/).some((s) => s === "..")) { + throw new Error("Path traversal not allowed"); + } + + if (path.isAbsolute(documentPath)) { + throw new Error("Absolute paths not allowed"); + } + + const fullPath = path.join(skill.skillDir, documentPath); + + if (!isPathWithinBase(fullPath, skillsDir)) { + throw new Error("Path escapes the skills directory"); + } + + 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`, + ); + } + + if (isText) { + return { text: fs.readFileSync(fullPath, "utf-8") }; + } else { + return { blob: fs.readFileSync(fullPath).toString("base64") }; + } +} + +/** + * Options for generateSkillIndex(). + */ +export interface GenerateSkillIndexOptions { + /** Archive declarations → per-skill `archives` entries. */ + archives?: SkillArchiveDeclaration[]; + /** + * Precomputed `sha256:{hex}` digests keyed by archive declaration, for + * callers that have already read the archive bytes (e.g. + * `registerSkillResources` reads them to serve). Entries without a + * precomputed digest fall back to reading the file. Keyed by declaration + * identity so two declarations sharing an `archivePath` don't collide. + */ + archiveDigests?: Map; +} + +/** + * Resolve an archive declaration's format, defaulting from the file + * extension when not explicitly set. + */ +function resolveArchiveFormat(decl: SkillArchiveDeclaration): ArchiveFormat { + if (decl.format) return decl.format; + const lower = decl.archivePath.toLowerCase(); + if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz")) return "tar.gz"; + if (lower.endsWith(".zip")) return "zip"; + throw new Error( + `Cannot infer archive format from path "${decl.archivePath}". Set format: "tar.gz" | "zip" explicitly.`, + ); +} + +/** + * Build the resource URI an archive is served under, per SEP-2640 + * (`skill://.`). + */ +function archiveResourceUri(decl: SkillArchiveDeclaration): string { + const format = resolveArchiveFormat(decl); + return `${SKILL_URI_SCHEME}${decl.skillPath}${archiveSuffix(format)}`; +} + +/** + * Validate an archive declaration against the SEP path/name rules and return + * the index `archives[]` reference for it. Reads the archive bytes to compute + * the digest unless `precomputedDigest` is supplied (when the caller has + * already read the same bytes to serve them — avoids a second read and any + * drift between the served bytes and the advertised digest). + */ +function archiveIndexRef( + decl: SkillArchiveDeclaration, + precomputedDigest?: string, +): { + url: string; + mimeType: string; + digest: string; +} { + // SEP constraint: final segment of skillPath MUST equal frontmatter name. + const finalSegment = decl.skillPath.split("/").pop()!; + if (finalSegment !== decl.name) { + throw new Error( + `Archive declaration: skillPath "${decl.skillPath}" final segment "${finalSegment}" does not match name "${decl.name}". Per SEP-2640, the final segment of the skill path MUST equal the frontmatter name.`, + ); + } + // SEP constraint: the name MUST satisfy the Agent Skills naming rule. + if (!isValidSkillName(decl.name)) { + throw new Error( + `Archive declaration: name "${decl.name}" violates the Agent Skills naming rule. Names must contain only lowercase letters, digits, and hyphens.`, + ); + } + let digest: string; + if (precomputedDigest !== undefined) { + digest = precomputedDigest; + } else { + let bytes: Buffer; + try { + bytes = fs.readFileSync(decl.archivePath); + } catch (err) { + throw new Error( + `Failed to read archive "${decl.archivePath}" for skill "${decl.name}": ${err instanceof Error ? err.message : String(err)}`, + ); + } + digest = sha256Digest(bytes); + } + return { + url: archiveResourceUri(decl), + mimeType: archiveMimeType(resolveArchiveFormat(decl)), + digest, + }; +} + +/** + * Generate the `skill://index.json` discovery index (SEP-2640). + * + * Emits one type-less entry per skill in `skillMap` — `{ frontmatter, url, + * digest }`, where `frontmatter` is the skill's full SKILL.md frontmatter + * copied verbatim — and one entry per archive declaration — + * `{ frontmatter, archives: [{ url, mimeType, digest }] }`. The index has no + * `$schema`/version marker. + */ +export function generateSkillIndex( + skillMap: Map, + options?: GenerateSkillIndexOptions, +): SkillIndex { + const opts = options ?? {}; + + const skillEntries: SkillIndexEntry[] = Array.from(skillMap.values()).map( + (skill) => ({ + frontmatter: skill.frontmatter, + url: buildSkillUri(skill.skillPath), + digest: skill.digest, + }), + ); + + const archiveEntries: SkillIndexEntry[] = (opts.archives ?? []).map((a) => ({ + frontmatter: a.frontmatter ?? { name: a.name, description: a.description }, + archives: [archiveIndexRef(a, opts.archiveDigests?.get(a))], + })); + + return { skills: [...skillEntries, ...archiveEntries] }; +} + +/** + * Register MCP resources for all discovered skills on an McpServer. + * + * Registers per-skill (using multi-segment skill paths): + * - skill://{skillPath}/SKILL.md — skill content (listed resource) + * + * Always registers: + * - skill://index.json — well-known discovery index (SEP enumeration) + * + * Optionally registers: + * - skill://{+skillFilePath} — catch-all template for supporting files. + * - A `resources/directory/read` handler (when `directoryRead: true`) so + * hosts can enumerate the files under each individually-served skill. + */ +export function registerSkillResources( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + server: any, + skillMap: Map, + skillsDir: string, + options?: RegisterSkillResourcesOptions, +): void { + const { + template = true, + index = true, + audience = ["assistant"], + archives = [], + directoryRead = false, + } = options ?? {}; + + // Compute the most recent lastModified across all skills for aggregate resources + const latestModified = skillMap.size > 0 + ? Array.from(skillMap.values()) + .map((s) => s.lastModified) + .sort() + .pop() + : undefined; + + // Register archive resources before the index, so the index can reference them. + // The digest of each archive is computed once here, from the same bytes we + // serve, and reused for the index — so the advertised digest can't drift from + // the served artifact (and the file is read only once). + const archiveDigests = new Map(); + for (const archive of archives) { + const format = resolveArchiveFormat(archive); + const uri = archiveResourceUri(archive); + const mimeType = archiveMimeType(format); + + let archiveBytes: Buffer; + try { + archiveBytes = fs.readFileSync(archive.archivePath); + } catch (err) { + throw new Error( + `Failed to read archive "${archive.archivePath}" for skill "${archive.name}": ${err instanceof Error ? err.message : String(err)}`, + ); + } + archiveDigests.set(archive, sha256Digest(archiveBytes)); + const archiveBase64 = archiveBytes.toString("base64"); + let archiveModified: string; + try { + archiveModified = fs.statSync(archive.archivePath).mtime.toISOString(); + } catch { + archiveModified = new Date().toISOString(); + } + + server.resource( + `${archive.name}-archive`, + uri, + { + description: `${archive.description} (archive distribution)`, + mimeType, + size: archiveBytes.length, + annotations: { + audience, + priority: 0.9, + lastModified: archiveModified, + }, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async (resourceUri: URL): Promise => ({ + contents: [ + { + uri: resourceUri.href, + mimeType, + blob: archiveBase64, + }, + ], + }), + ); + } + + // Register per-skill resources + for (const [skillPath, skill] of skillMap) { + const skillAudience = skill.audience ?? audience; + + server.resource( + skill.name, + `skill://${skillPath}/SKILL.md`, + { + description: skill.description, + mimeType: "text/markdown", + size: skill.size, + annotations: { + audience: skillAudience, + priority: 1.0, + lastModified: skill.lastModified, + }, + ...(skill.meta ? { _meta: skill.meta } : {}), + }, + async (uri: URL) => { + try { + const content = loadSkillContent(skill.absolutePath, 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 "${skill.name}": ${message}`, + }, + ], + }; + } + }, + ); + } + + // Well-known discovery index (SEP enumeration mechanism). Optional — + // servers with unenumerable catalogs can pass `index: false`. + if (index) { + const indexJson = generateSkillIndex(skillMap, { archives, archiveDigests }); + const indexJsonStr = JSON.stringify(indexJson, null, 2); + server.resource( + "skills-index", + INDEX_JSON_URI, + { + description: + "Discovery index of available skills served by this server (skill://index.json)", + mimeType: "application/json", + size: Buffer.byteLength(indexJsonStr), + annotations: { + audience: ["assistant"], + priority: 0.8, + lastModified: latestModified, + }, + }, + async (uri: URL) => ({ + contents: [ + { + uri: uri.href, + text: indexJsonStr, + }, + ], + }), + ); + } + + // SEP-2640 `resources/directory/read`: enumerate the files under each + // individually-served skill directory. Registered on the low-level request + // router (this is an extension method, not part of the high-level resource + // API). The server MUST also advertise the capability via + // `declareSkillsExtension(server, { directoryRead: true })` before connect. + if (directoryRead) { + const handler = makeDirectoryReadHandler(skillMap); + // McpServer exposes the underlying low-level Server as `.server`. + const lowLevel = server.server ?? server; + + // The handler is useless unless the capability was also advertised in the + // initialize handshake — well-behaved clients gate on it and will never + // call an undeclared method. Warn loudly rather than fail silently. + const declared = + lowLevel.getCapabilities?.()?.extensions?.[SKILLS_EXTENSION_ID] + ?.directoryRead === true; + if (!declared) { + console.error( + `[skills] registerSkillResources({ directoryRead: true }) installed the resources/directory/read handler, but the capability is not declared. ` + + `Call declareSkillsExtension(server, { directoryRead: true }) BEFORE server.connect() — otherwise clients will never invoke it.`, + ); + } + + lowLevel.setRequestHandler(DirectoryReadRequestSchema, handler); + } + + // Catch-all resource template for supporting files. + if (template) { + server.resource( + "skill-file", + new ResourceTemplate("skill://{+skillFilePath}", { + list: undefined, + complete: { + skillFilePath: (value) => { + // Provide completions: all known skill paths + their files + const completions: string[] = []; + for (const [sp, skill] of skillMap) { + if (skill.documents.length === 0) continue; + for (const doc of skill.documents) { + const fullPath = `${sp}/${doc.path}`; + if (fullPath.startsWith(value)) { + completions.push(fullPath); + } + } + } + return completions; + }, + }, + }), + { + description: "Fetch a supporting file from a skill directory", + mimeType: "text/plain", + annotations: { + audience, + priority: 0.2, + lastModified: latestModified, + }, + }, + async (uri: URL, variables: Record) => { + const skillFilePath = Array.isArray(variables.skillFilePath) + ? variables.skillFilePath[0] + : variables.skillFilePath; + + // Resolve the skill path using longest-prefix match + const knownPaths = Array.from(skillMap.keys()).sort( + (a, b) => b.length - a.length, + ); + let matchedSkill: SkillMetadata | undefined; + let filePath: string | undefined; + + for (const sp of knownPaths) { + if (skillFilePath.startsWith(sp + "/")) { + matchedSkill = skillMap.get(sp); + filePath = skillFilePath.slice(sp.length + 1); + break; + } + } + + if (!matchedSkill || !filePath) { + return { + contents: [ + { + uri: uri.href, + text: `# Error\n\nCould not resolve skill path from "${skillFilePath}"`, + }, + ], + }; + } + + const doc = matchedSkill.documents.find((d) => d.path === filePath); + if (!doc) { + const available = + matchedSkill.documents.map((d) => `- ${d.path}`).join("\n"); + return { + contents: [ + { + uri: uri.href, + text: `# Error\n\nFile "${filePath}" not found in skill "${matchedSkill.name}".\n\n## Available Files\n\n${available || "No supporting files available."}`, + }, + ], + }; + } + + try { + const isText = isTextMimeType(doc.mimeType); + const content = loadDocument( + matchedSkill, + filePath, + skillsDir, + isText, + ); + return { + contents: [ + { + uri: uri.href, + mimeType: doc.mimeType, + ...content, + }, + ], + }; + } catch (error) { + const message = + error instanceof Error ? error.message : String(error); + return { + contents: [ + { + uri: uri.href, + text: `# Error\n\nFailed to read file: ${message}`, + }, + ], + }; + } + }, + ); + } +} diff --git a/typescript/sdk/src/archive.test.ts b/typescript/sdk/src/archive.test.ts new file mode 100644 index 0000000..bc4369a --- /dev/null +++ b/typescript/sdk/src/archive.test.ts @@ -0,0 +1,401 @@ +import { describe, expect, it } from "vitest"; +import { Readable } from "node:stream"; +import * as zlib from "node:zlib"; +import { pack as tarPack } from "tar-stream"; +import { ZipFile } from "yazl"; +import { + detectArchiveFormat, + stripArchiveSuffix, + archiveMimeType, + archiveSuffix, + extractSkillArchive, +} from "./archive.js"; + +// --------------------------------------------------------------------------- +// Archive builders (test fixtures) +// --------------------------------------------------------------------------- + +interface FakeEntry { + name: string; + data: Buffer | string; + type?: "file" | "symlink" | "directory"; + linkname?: string; +} + +async function buildTarGz(entries: FakeEntry[]): Promise { + const pack = tarPack(); + for (const e of entries) { + if (e.type === "symlink") { + pack.entry({ + name: e.name, + type: "symlink", + linkname: e.linkname ?? "", + }); + continue; + } + if (e.type === "directory") { + pack.entry({ name: e.name, type: "directory" }); + continue; + } + const data = typeof e.data === "string" ? Buffer.from(e.data) : e.data; + pack.entry({ name: e.name, size: data.length }, data); + } + pack.finalize(); + + const chunks: Buffer[] = []; + for await (const chunk of pack as unknown as AsyncIterable) { + chunks.push(chunk); + } + return zlib.gzipSync(Buffer.concat(chunks)); +} + +async function buildZip(entries: FakeEntry[]): Promise { + const zip = new ZipFile(); + for (const e of entries) { + const data = typeof e.data === "string" ? Buffer.from(e.data) : e.data; + zip.addBuffer(data, e.name); + } + zip.end(); + + const chunks: Buffer[] = []; + for await (const chunk of zip.outputStream as unknown as Readable) { + chunks.push(chunk as Buffer); + } + return Buffer.concat(chunks); +} + +// --------------------------------------------------------------------------- +// detectArchiveFormat +// --------------------------------------------------------------------------- + +describe("detectArchiveFormat", () => { + it("detects from MIME type first", () => { + expect(detectArchiveFormat("application/gzip", undefined)).toBe("tar.gz"); + expect(detectArchiveFormat("application/zip", undefined)).toBe("zip"); + }); + + it("falls back to URL suffix", () => { + expect(detectArchiveFormat(undefined, "skill://x.tar.gz")).toBe("tar.gz"); + expect(detectArchiveFormat(undefined, "skill://x.tgz")).toBe("tar.gz"); + expect(detectArchiveFormat(undefined, "skill://x.zip")).toBe("zip"); + }); + + it("MIME type wins over suffix", () => { + expect(detectArchiveFormat("application/zip", "skill://x.tar.gz")).toBe("zip"); + }); + + it("returns null when neither signal identifies a format", () => { + expect(detectArchiveFormat(undefined, "skill://x")).toBeNull(); + expect(detectArchiveFormat("text/markdown", "skill://x.md")).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// stripArchiveSuffix +// --------------------------------------------------------------------------- + +describe("stripArchiveSuffix", () => { + it("strips .tar.gz", () => { + expect(stripArchiveSuffix("skill://pdf-processing.tar.gz")).toBe( + "skill://pdf-processing", + ); + }); + + it("strips .tgz", () => { + expect(stripArchiveSuffix("skill://x.tgz")).toBe("skill://x"); + }); + + it("strips .zip", () => { + expect(stripArchiveSuffix("skill://acme/billing/refunds.zip")).toBe( + "skill://acme/billing/refunds", + ); + }); + + it("returns input unchanged when no suffix matches", () => { + expect(stripArchiveSuffix("skill://x/SKILL.md")).toBe("skill://x/SKILL.md"); + }); +}); + +// --------------------------------------------------------------------------- +// archiveMimeType / archiveSuffix +// --------------------------------------------------------------------------- + +describe("archive format helpers", () => { + it("archiveMimeType", () => { + expect(archiveMimeType("tar.gz")).toBe("application/gzip"); + expect(archiveMimeType("zip")).toBe("application/zip"); + }); + + it("archiveSuffix", () => { + expect(archiveSuffix("tar.gz")).toBe(".tar.gz"); + expect(archiveSuffix("zip")).toBe(".zip"); + }); +}); + +// --------------------------------------------------------------------------- +// extractSkillArchive — happy path +// --------------------------------------------------------------------------- + +describe("extractSkillArchive (.tar.gz)", () => { + it("extracts files keyed by relative path", async () => { + const tarball = await buildTarGz([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---\nbody" }, + { name: "references/REF.md", data: "ref content" }, + ]); + + const archive = await extractSkillArchive(tarball, { + mimeType: "application/gzip", + }); + + expect(archive.files.get("SKILL.md")?.toString("utf-8")).toContain("body"); + expect(archive.files.get("references/REF.md")?.toString("utf-8")).toBe( + "ref content", + ); + expect(archive.totalSize).toBeGreaterThan(0); + }); + + it("falls back to URL suffix when mimeType is missing", async () => { + const tarball = await buildTarGz([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + ]); + const archive = await extractSkillArchive(tarball, { + url: "skill://x.tar.gz", + }); + expect(archive.files.has("SKILL.md")).toBe(true); + }); + + it("skips directory entries", async () => { + const tarball = await buildTarGz([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + { name: "subdir/", type: "directory", data: "" }, + { name: "subdir/inner.txt", data: "inner" }, + ]); + const archive = await extractSkillArchive(tarball, { + mimeType: "application/gzip", + }); + expect(archive.files.has("subdir/")).toBe(false); + expect(archive.files.get("subdir/inner.txt")?.toString()).toBe("inner"); + }); +}); + +describe("extractSkillArchive (.zip)", () => { + it("extracts files keyed by relative path", async () => { + const zipBuf = await buildZip([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---\nbody" }, + { name: "references/REF.md", data: "ref content" }, + ]); + + const archive = await extractSkillArchive(zipBuf, { + mimeType: "application/zip", + }); + + expect(archive.files.get("SKILL.md")?.toString("utf-8")).toContain("body"); + expect(archive.files.get("references/REF.md")?.toString("utf-8")).toBe( + "ref content", + ); + }); + + it("falls back to URL suffix when mimeType is missing", async () => { + const zipBuf = await buildZip([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + ]); + const archive = await extractSkillArchive(zipBuf, { + url: "skill://x.zip", + }); + expect(archive.files.has("SKILL.md")).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// Archive safety +// --------------------------------------------------------------------------- + +describe("archive safety (.tar.gz)", () => { + it("rejects path-traversal segments", async () => { + const tarball = await buildTarGz([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + { name: "../escape.txt", data: "evil" }, + ]); + await expect( + extractSkillArchive(tarball, { mimeType: "application/gzip" }), + ).rejects.toThrow(/Invalid archive entry path/); + }); + + it("rejects absolute paths", async () => { + const tarball = await buildTarGz([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + { name: "/etc/passwd", data: "evil" }, + ]); + await expect( + extractSkillArchive(tarball, { mimeType: "application/gzip" }), + ).rejects.toThrow(/Invalid archive entry path/); + }); + + it("rejects symlinks resolving outside the skill directory", async () => { + const tarball = await buildTarGz([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + { name: "evil-link", type: "symlink", data: "", linkname: "../../etc/passwd" }, + ]); + await expect( + extractSkillArchive(tarball, { mimeType: "application/gzip" }), + ).rejects.toThrow(/resolves outside skill directory/); + }); + + it("rejects archive without SKILL.md at root", async () => { + const tarball = await buildTarGz([ + { name: "wrapper/SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + ]); + await expect( + extractSkillArchive(tarball, { mimeType: "application/gzip" }), + ).rejects.toThrow(/SKILL\.md at its root/); + }); + + it("enforces maxFileSize", async () => { + const big = Buffer.alloc(2 * 1024 * 1024); // 2MB + const tarball = await buildTarGz([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + { name: "big.bin", data: big }, + ]); + await expect( + extractSkillArchive( + tarball, + { mimeType: "application/gzip" }, + { maxFileSize: 1024 * 1024 }, + ), + ).rejects.toThrow(/maxFileSize/); + }); + + it("enforces maxTotalSize", async () => { + const tarball = await buildTarGz([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + { name: "a.txt", data: Buffer.alloc(600 * 1024) }, + { name: "b.txt", data: Buffer.alloc(600 * 1024) }, + ]); + await expect( + extractSkillArchive( + tarball, + { mimeType: "application/gzip" }, + { maxTotalSize: 1024 * 1024 }, + ), + ).rejects.toThrow(/maxTotalSize|exceeds maxTotalSize/); + }); + + it("rejects a gzip bomb before fully inflating it", async () => { + // 4 MB of zeros compresses to a few KB but decompresses well past the + // 1 MB bound. The tar.gz path must abort while *streaming* the inflated + // bytes, not gunzip the whole payload into memory first. + const bomb = await buildTarGz([ + { name: "SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + { name: "zeros.bin", data: Buffer.alloc(4 * 1024 * 1024) }, + ]); + expect(bomb.length).toBeLessThan(1024 * 1024); // tiny compressed + await expect( + extractSkillArchive( + bomb, + { mimeType: "application/gzip" }, + { maxTotalSize: 1024 * 1024, maxFileSize: 8 * 1024 * 1024 }, + ), + ).rejects.toThrow(/maxTotalSize/); + }); +}); + +/** + * Hand-craft a minimal stored-method (no compression) zip with a single + * file at the given path. Used to test extractor safety against names + * that the well-behaved `yazl` writer refuses to produce. + */ +function craftMaliciousZip(fileName: string, content: string): Buffer { + const data = Buffer.from(content); + const nameBuf = Buffer.from(fileName); + // Stored method, no encryption, no compression + const localHeader = Buffer.alloc(30); + localHeader.writeUInt32LE(0x04034b50, 0); // local file header signature + localHeader.writeUInt16LE(20, 4); // version needed + localHeader.writeUInt16LE(0, 6); // flags + localHeader.writeUInt16LE(0, 8); // method = stored + localHeader.writeUInt16LE(0, 10); // mtime + localHeader.writeUInt16LE(0, 12); // mdate + localHeader.writeUInt32LE(0, 14); // crc32 (yauzl tolerates 0 with no validateEntrySizes) + localHeader.writeUInt32LE(data.length, 18); // compressed size + localHeader.writeUInt32LE(data.length, 22); // uncompressed size + localHeader.writeUInt16LE(nameBuf.length, 26); // filename length + localHeader.writeUInt16LE(0, 28); // extra length + + const localOffset = 0; + const central = Buffer.alloc(46); + central.writeUInt32LE(0x02014b50, 0); // central dir signature + central.writeUInt16LE(20, 4); // version made by + central.writeUInt16LE(20, 6); // version needed + central.writeUInt16LE(0, 8); // flags + central.writeUInt16LE(0, 10); // method + central.writeUInt16LE(0, 12); // mtime + central.writeUInt16LE(0, 14); // mdate + central.writeUInt32LE(0, 16); // crc32 + central.writeUInt32LE(data.length, 20); // compressed size + central.writeUInt32LE(data.length, 24); // uncompressed size + central.writeUInt16LE(nameBuf.length, 28); // filename length + central.writeUInt16LE(0, 30); // extra length + central.writeUInt16LE(0, 32); // comment length + central.writeUInt16LE(0, 34); // disk number + central.writeUInt16LE(0, 36); // internal attrs + central.writeUInt32LE(0, 38); // external attrs + central.writeUInt32LE(localOffset, 42); // local header offset + + const before = Buffer.concat([localHeader, nameBuf, data]); + const cdEntry = Buffer.concat([central, nameBuf]); + + const eocd = Buffer.alloc(22); + eocd.writeUInt32LE(0x06054b50, 0); // EOCD signature + eocd.writeUInt16LE(0, 4); // disk number + eocd.writeUInt16LE(0, 6); // disk where central dir starts + eocd.writeUInt16LE(1, 8); // entries on this disk + eocd.writeUInt16LE(1, 10); // total entries + eocd.writeUInt32LE(cdEntry.length, 12); // central dir size + eocd.writeUInt32LE(before.length, 16); // central dir offset + eocd.writeUInt16LE(0, 20); // comment length + + return Buffer.concat([before, cdEntry, eocd]); +} + +describe("archive safety (.zip)", () => { + // yauzl's own filename validation rejects most malicious names before our + // extractor's `validateEntryPath` runs. Either layer is acceptable + // defense — the test asserts only that the archive is rejected, not which + // layer caught it. + const REJECTED = /Invalid archive entry path|invalid relative path|absolute path/; + + it("rejects path-traversal segments", async () => { + const zipBuf = craftMaliciousZip("../escape.txt", "evil"); + await expect( + extractSkillArchive(zipBuf, { mimeType: "application/zip" }), + ).rejects.toThrow(REJECTED); + }); + + it("rejects absolute paths", async () => { + const zipBuf = craftMaliciousZip("/etc/passwd", "evil"); + await expect( + extractSkillArchive(zipBuf, { mimeType: "application/zip" }), + ).rejects.toThrow(REJECTED); + }); + + it("rejects archive without SKILL.md at root", async () => { + const zipBuf = await buildZip([ + { name: "wrapper/SKILL.md", data: "---\nname: x\ndescription: y\n---" }, + ]); + await expect( + extractSkillArchive(zipBuf, { mimeType: "application/zip" }), + ).rejects.toThrow(/SKILL\.md at its root/); + }); +}); + +// --------------------------------------------------------------------------- +// Format detection failure +// --------------------------------------------------------------------------- + +describe("extractSkillArchive (unknown format)", () => { + it("throws when neither mimeType nor URL suffix identifies a format", async () => { + await expect( + extractSkillArchive(Buffer.from("nope"), { url: "skill://x" }), + ).rejects.toThrow(/Cannot determine archive format/); + }); +}); diff --git a/typescript/sdk/src/archive.ts b/typescript/sdk/src/archive.ts new file mode 100644 index 0000000..23fded4 --- /dev/null +++ b/typescript/sdk/src/archive.ts @@ -0,0 +1,403 @@ +/** + * Archive extraction for skill distribution per SEP-2640. + * + * The SEP defines `type: "archive"` as a normative entry type in + * `skill://index.json`: a single resource (`.tar.gz` or `.zip`) that + * unpacks to a skill directory. This module provides in-memory extraction + * with the Agent Skills archive safety requirements: + * + * - reject path-traversal sequences (..) + * - reject absolute paths + * - reject symlinks resolving outside the skill directory + * - bound total uncompressed size (decompression-bomb defense) + * - bound per-file size and entry count + * + * Hosts MUST support both formats. SDK consumers normally call + * `readSkillArchive()` (client.ts), which fetches the archive via + * `resources/read` and dispatches here based on `mimeType`. + */ + +import * as zlib from "node:zlib"; +import { Readable } from "node:stream"; +import { extract as tarExtract } from "tar-stream"; +import yauzl from "yauzl"; +import type { + ArchiveFormat, + ExtractArchiveOptions, + UnpackedSkillArchive, +} from "./types.js"; + +const DEFAULT_MAX_TOTAL_SIZE = 50 * 1024 * 1024; +const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; +const DEFAULT_MAX_ENTRIES = 1024; + +const TAR_GZ_MIME = "application/gzip"; +const ZIP_MIME = "application/zip"; + +/** + * Detect archive format from MIME type, falling back to URL suffix. + * + * Per SEP-2640: "Hosts SHOULD determine the format from the resource's + * mimeType, falling back to the URL suffix." + * + * Returns `null` if neither signal identifies a supported format. + */ +export function detectArchiveFormat( + mimeType: string | undefined, + url: string | undefined, +): ArchiveFormat | null { + if (mimeType === TAR_GZ_MIME) return "tar.gz"; + if (mimeType === ZIP_MIME) return "zip"; + if (url) { + if (url.endsWith(".tar.gz") || url.endsWith(".tgz")) return "tar.gz"; + if (url.endsWith(".zip")) return "zip"; + } + return null; +} + +/** + * Strip the archive suffix from a URL to get the post-unpack skill base. + * + * Per SEP-2640: `skill://pdf-processing.tar.gz` unpacks to + * `skill://pdf-processing/`; this returns the URL with `.tar.gz` / + * `.tgz` / `.zip` removed. The post-unpack skill path is whatever + * follows the `skill://` scheme prefix. + */ +export function stripArchiveSuffix(url: string): string { + if (url.endsWith(".tar.gz")) return url.slice(0, -".tar.gz".length); + if (url.endsWith(".tgz")) return url.slice(0, -".tgz".length); + if (url.endsWith(".zip")) return url.slice(0, -".zip".length); + return url; +} + +/** MIME type for an archive format. */ +export function archiveMimeType(format: ArchiveFormat): string { + return format === "tar.gz" ? TAR_GZ_MIME : ZIP_MIME; +} + +/** URL suffix for an archive format. */ +export function archiveSuffix(format: ArchiveFormat): string { + return format === "tar.gz" ? ".tar.gz" : ".zip"; +} + +/** + * Validate a relative path from an archive entry. + * + * Returns the normalized (forward-slash) path, or `null` if the entry + * violates archive safety: absolute paths, drive letters, `..` segments, + * or empty paths are all rejected. + */ +function validateEntryPath(entryPath: string): string | null { + if (!entryPath) return null; + const normalized = entryPath.replace(/\\/g, "/").replace(/\/+$/, ""); + if (!normalized) return null; + if (normalized.startsWith("/")) return null; + if (/^[a-zA-Z]:/.test(normalized)) return null; + const segments = normalized.split("/"); + if (segments.some((s) => s === "..")) return null; + return normalized; +} + +function resolvedOptions( + options?: ExtractArchiveOptions, +): Required { + return { + maxTotalSize: options?.maxTotalSize ?? DEFAULT_MAX_TOTAL_SIZE, + maxFileSize: options?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE, + maxEntries: options?.maxEntries ?? DEFAULT_MAX_ENTRIES, + }; +} + +/** + * Extract a `.tar.gz` archive from an in-memory buffer. + */ +function extractTarGz( + data: Buffer, + options: Required, +): Promise { + return new Promise((resolve, reject) => { + const files = new Map(); + let totalSize = 0; + let entryCount = 0; + let aborted = false; + let decompressedBytes = 0; + + const gunzip = zlib.createGunzip(); + const extractor = tarExtract(); + + const abort = (err: Error) => { + if (aborted) return; + aborted = true; + gunzip.destroy(); + extractor.destroy(); + reject(err); + }; + + // Decompression-bomb defense: bound the *decompressed* byte count as it + // streams out of gunzip, rather than inflating the whole (possibly + // gigabyte) payload into memory up front with gunzipSync. We abort as soon + // as the inflated size crosses maxTotalSize, mirroring the incremental + // size check on the zip path. + gunzip.on("data", (chunk: Buffer) => { + decompressedBytes += chunk.length; + if (decompressedBytes > options.maxTotalSize) { + abort( + new Error( + `Decompressed archive size exceeds maxTotalSize (${options.maxTotalSize})`, + ), + ); + } + }); + gunzip.on("error", (err) => { + abort( + new Error( + `Failed to gunzip tar.gz archive: ${err instanceof Error ? err.message : String(err)}`, + ), + ); + }); + + extractor.on("entry", (header, stream, next) => { + if (aborted) { + stream.resume(); + next(); + return; + } + + if (header.type === "symlink" || header.type === "link") { + // Per SEP archive safety: reject links resolving outside the + // skill directory. Validate the link target the same way as a + // regular path; any traversal aborts. + const target = validateEntryPath(header.linkname ?? ""); + stream.resume(); + if (target === null) { + abort( + new Error( + `Archive link target "${header.linkname ?? ""}" resolves outside skill directory`, + ), + ); + return; + } + next(); + return; + } + + if (header.type !== "file") { + stream.resume(); + next(); + return; + } + + const entryPath = validateEntryPath(header.name); + if (entryPath === null) { + stream.resume(); + abort(new Error(`Invalid archive entry path: "${header.name}"`)); + return; + } + + entryCount += 1; + if (entryCount > options.maxEntries) { + stream.resume(); + abort( + new Error( + `Archive entry count exceeds maxEntries (${options.maxEntries})`, + ), + ); + return; + } + + const chunks: Buffer[] = []; + let entrySize = 0; + stream.on("data", (chunk: Buffer) => { + entrySize += chunk.length; + if (entrySize > options.maxFileSize) { + abort( + new Error( + `Archive entry "${entryPath}" exceeds maxFileSize (${options.maxFileSize})`, + ), + ); + return; + } + if (totalSize + entrySize > options.maxTotalSize) { + abort( + new Error( + `Archive total size exceeds maxTotalSize (${options.maxTotalSize})`, + ), + ); + return; + } + chunks.push(chunk); + }); + stream.on("end", () => { + if (aborted) return; + files.set(entryPath, Buffer.concat(chunks)); + totalSize += entrySize; + next(); + }); + stream.on("error", abort); + }); + + extractor.on("finish", () => { + if (!aborted) resolve({ files, totalSize }); + }); + extractor.on("error", abort); + + Readable.from(data).pipe(gunzip).pipe(extractor); + }); +} + +/** + * Extract a `.zip` archive from an in-memory buffer. + */ +function extractZip( + data: Buffer, + options: Required, +): Promise { + return new Promise((resolve, reject) => { + yauzl.fromBuffer(data, { lazyEntries: true }, (err, zipfile) => { + if (err || !zipfile) { + reject( + new Error( + `Failed to open zip archive: ${err?.message ?? "unknown error"}`, + ), + ); + return; + } + + const files = new Map(); + let totalSize = 0; + let entryCount = 0; + let aborted = false; + + const abort = (e: Error) => { + if (aborted) return; + aborted = true; + zipfile.close(); + reject(e); + }; + + zipfile.on("error", abort); + zipfile.on("end", () => { + if (!aborted) resolve({ files, totalSize }); + }); + + zipfile.on("entry", (entry) => { + if (aborted) return; + + // Directory entry — skip but continue + if (/\/$/.test(entry.fileName)) { + zipfile.readEntry(); + return; + } + + const entryPath = validateEntryPath(entry.fileName); + if (entryPath === null) { + abort(new Error(`Invalid archive entry path: "${entry.fileName}"`)); + return; + } + + entryCount += 1; + if (entryCount > options.maxEntries) { + abort( + new Error( + `Archive entry count exceeds maxEntries (${options.maxEntries})`, + ), + ); + return; + } + + // Pre-flight check: refuse entries that claim oversize before we + // even open the read stream. zip headers carry uncompressedSize. + if (entry.uncompressedSize > options.maxFileSize) { + abort( + new Error( + `Archive entry "${entryPath}" declares size ${entry.uncompressedSize}, exceeds maxFileSize (${options.maxFileSize})`, + ), + ); + return; + } + if (totalSize + entry.uncompressedSize > options.maxTotalSize) { + abort( + new Error( + `Archive total size exceeds maxTotalSize (${options.maxTotalSize})`, + ), + ); + return; + } + + zipfile.openReadStream(entry, (streamErr, stream) => { + if (streamErr || !stream) { + abort( + new Error( + `Failed to read zip entry "${entryPath}": ${streamErr?.message ?? "unknown"}`, + ), + ); + return; + } + + const chunks: Buffer[] = []; + let entrySize = 0; + stream.on("data", (chunk: Buffer) => { + entrySize += chunk.length; + // Catch decompression bombs that lie about uncompressed size + if (entrySize > options.maxFileSize) { + abort( + new Error( + `Archive entry "${entryPath}" actual size exceeds maxFileSize (${options.maxFileSize})`, + ), + ); + return; + } + chunks.push(chunk); + }); + stream.on("end", () => { + if (aborted) return; + files.set(entryPath, Buffer.concat(chunks)); + totalSize += entrySize; + zipfile.readEntry(); + }); + stream.on("error", abort); + }); + }); + + zipfile.readEntry(); + }); + }); +} + +/** + * Extract a skill archive from an in-memory buffer. + * + * Format is determined from `mimeType` first, then falls back to URL + * suffix per SEP-2640. Throws if the format cannot be determined. + * + * Applies archive safety: rejects path traversal, absolute paths, + * symlinks resolving outside the skill directory, and decompression + * bombs (via per-file, total-size, and entry-count bounds). + */ +export async function extractSkillArchive( + data: Buffer, + context: { mimeType?: string; url?: string }, + options?: ExtractArchiveOptions, +): Promise { + const format = detectArchiveFormat(context.mimeType, context.url); + if (format === null) { + throw new Error( + `Cannot determine archive format from mimeType="${context.mimeType ?? ""}" and url="${context.url ?? ""}". Per SEP-2640, archives must be application/gzip (.tar.gz) or application/zip (.zip).`, + ); + } + + const opts = resolvedOptions(options); + const archive = format === "tar.gz" + ? await extractTarGz(data, opts) + : await extractZip(data, opts); + + // Per SEP-2640: "SKILL.md MUST be at the archive root, not nested + // inside a wrapper directory." + if (!archive.files.has("SKILL.md") && !archive.files.has("skill.md")) { + throw new Error( + "Archive does not contain SKILL.md at its root. Per SEP-2640, archives MUST place SKILL.md at the archive root, not inside a wrapper directory.", + ); + } + + return archive; +} diff --git a/typescript/sdk/src/client.test.ts b/typescript/sdk/src/client.test.ts new file mode 100644 index 0000000..9b575be --- /dev/null +++ b/typescript/sdk/src/client.test.ts @@ -0,0 +1,277 @@ +import { describe, it, expect } from "vitest"; +import { parseSkillFrontmatter, buildSkillsSummary, buildSkillsCatalog } from "./_client.js"; +import type { SkillSummary } from "./types.js"; + +// --------------------------------------------------------------------------- +// parseSkillFrontmatter +// --------------------------------------------------------------------------- + +describe("parseSkillFrontmatter", () => { + it("extracts name and description", () => { + const content = `--- +name: code-review +description: Review code for quality +--- +# Code Review +`; + expect(parseSkillFrontmatter(content)).toEqual({ + name: "code-review", + description: "Review code for quality", + }); + }); + + it("strips quotes from values", () => { + const content = `--- +name: "my-skill" +description: 'A quoted description' +--- +`; + expect(parseSkillFrontmatter(content)).toEqual({ + name: "my-skill", + description: "A quoted description", + }); + }); + + it("returns empty description when missing", () => { + const content = `--- +name: minimal +--- +`; + expect(parseSkillFrontmatter(content)).toEqual({ + name: "minimal", + description: "", + }); + }); + + it("returns null when no frontmatter", () => { + expect(parseSkillFrontmatter("# Just a heading")).toBeNull(); + expect(parseSkillFrontmatter("")).toBeNull(); + }); + + it("returns null when frontmatter is not closed", () => { + expect(parseSkillFrontmatter("---\nname: broken\n")).toBeNull(); + }); + + it("returns null when name is missing", () => { + const content = `--- +description: no name here +--- +`; + expect(parseSkillFrontmatter(content)).toBeNull(); + }); + + it("handles extra frontmatter fields gracefully", () => { + const content = `--- +name: full +description: Has extras +metadata: + author: test + version: "1.0" +--- +`; + expect(parseSkillFrontmatter(content)).toEqual({ + name: "full", + description: "Has extras", + }); + }); + + it("handles --- in body content", () => { + const content = `--- +name: tricky +description: Has dashes +--- +# Heading + +Some text with --- in it. +`; + expect(parseSkillFrontmatter(content)).toEqual({ + name: "tricky", + description: "Has dashes", + }); + }); + + it("handles a YAML literal-block multi-line description", () => { + const content = `--- +name: multiline +description: | + First line of description. + Second line of description. +--- +# Body +`; + expect(parseSkillFrontmatter(content)).toEqual({ + name: "multiline", + description: "First line of description.\nSecond line of description.", + }); + }); + + it("handles a YAML folded-block multi-line description", () => { + const content = `--- +name: folded +description: > + This description + folds onto one line + with spaces. +--- +`; + const result = parseSkillFrontmatter(content); + expect(result?.name).toBe("folded"); + expect(result?.description).toContain("folds onto one line"); + }); +}); + +// --------------------------------------------------------------------------- +// buildSkillsSummary +// --------------------------------------------------------------------------- + +describe("buildSkillsSummary", () => { + it("returns empty message for no skills", () => { + expect(buildSkillsSummary([])).toBe("No skills available."); + }); + + it("formats single-segment skill", () => { + const skills: SkillSummary[] = [ + { name: "code-review", skillPath: "code-review", uri: "skill://code-review/SKILL.md", description: "Review code" }, + ]; + const summary = buildSkillsSummary(skills); + expect(summary).toContain("code-review"); + expect(summary).toContain("skill://code-review/SKILL.md"); + expect(summary).toContain("Review code"); + // No [path: ...] when name equals skillPath + expect(summary).not.toContain("[path:"); + }); + + it("shows path info when name differs from skillPath", () => { + const skills: SkillSummary[] = [ + { name: "refunds", skillPath: "acme/billing/refunds", uri: "skill://acme/billing/refunds/SKILL.md", description: "Refunds" }, + ]; + const summary = buildSkillsSummary(skills); + expect(summary).toContain("[path: acme/billing/refunds]"); + }); + + it("handles skills without description", () => { + const skills: SkillSummary[] = [ + { name: "bare", skillPath: "bare", uri: "skill://bare/SKILL.md" }, + ]; + const summary = buildSkillsSummary(skills); + expect(summary).toContain("bare"); + expect(summary).not.toContain("undefined"); + }); +}); + +// --------------------------------------------------------------------------- +// buildSkillsCatalog +// --------------------------------------------------------------------------- + +describe("buildSkillsCatalog", () => { + const catalogOptions = { toolName: "ReadMcpResourceTool", serverName: "skills-server" }; + + it("returns empty string for no skills", () => { + expect(buildSkillsCatalog([], catalogOptions)).toBe(""); + }); + + it("includes tool name and server name in behavioral instructions", () => { + const skills: SkillSummary[] = [ + { name: "code-review", skillPath: "code-review", uri: "skill://code-review/SKILL.md", description: "Review code" }, + ]; + const catalog = buildSkillsCatalog(skills, catalogOptions); + expect(catalog).toContain("`ReadMcpResourceTool`"); + expect(catalog).toContain("`skills-server`"); + expect(catalog).toContain("with server `skills-server`"); + }); + + it("omits the server clause when serverName is not provided", () => { + const skills: SkillSummary[] = [ + { name: "code-review", skillPath: "code-review", uri: "skill://code-review/SKILL.md", description: "Review code" }, + ]; + const catalog = buildSkillsCatalog(skills, { toolName: "read_skill" }); + expect(catalog).toContain("`read_skill`"); + expect(catalog).not.toContain("with server"); + expect(catalog).toContain("with the skill's URI"); + }); + + it("includes XML skill entries with name, path, description, and uri", () => { + const skills: SkillSummary[] = [ + { name: "code-review", skillPath: "code-review", uri: "skill://code-review/SKILL.md", description: "Review code" }, + ]; + const catalog = buildSkillsCatalog(skills, catalogOptions); + expect(catalog).toContain(""); + expect(catalog).toContain(""); + expect(catalog).toContain("code-review"); + expect(catalog).toContain("Review code"); + expect(catalog).toContain("skill://code-review/SKILL.md"); + }); + + it("does NOT emit per entry by default", () => { + const skills: SkillSummary[] = [ + { name: "a", skillPath: "a", uri: "skill://a/SKILL.md", description: "A" }, + { name: "b", skillPath: "b", uri: "skill://b/SKILL.md", description: "B" }, + ]; + const catalog = buildSkillsCatalog(skills, catalogOptions); + expect(catalog).not.toContain(""); + // Wrapper-level mention still present + expect(catalog).toContain("with server `skills-server`"); + }); + + it("emits per entry when serverInEntries is true and serverName is set", () => { + const skills: SkillSummary[] = [ + { name: "a", skillPath: "a", uri: "skill://a/SKILL.md", description: "A" }, + { name: "b", skillPath: "b", uri: "skill://b/SKILL.md", description: "B" }, + ]; + const catalog = buildSkillsCatalog(skills, { + ...catalogOptions, + serverInEntries: true, + }); + const matches = catalog.match(/skills-server<\/server>/g) ?? []; + expect(matches).toHaveLength(2); + }); + + it("ignores serverInEntries when serverName is absent", () => { + const skills: SkillSummary[] = [ + { name: "a", skillPath: "a", uri: "skill://a/SKILL.md", description: "A" }, + ]; + const catalog = buildSkillsCatalog(skills, { + toolName: "read_skill", + serverInEntries: true, + }); + expect(catalog).not.toContain(""); + }); + + it("includes multi-segment skill path", () => { + const skills: SkillSummary[] = [ + { name: "refunds", skillPath: "acme/billing/refunds", uri: "skill://acme/billing/refunds/SKILL.md", description: "Process refunds" }, + ]; + const catalog = buildSkillsCatalog(skills, catalogOptions); + expect(catalog).toContain("acme/billing/refunds"); + expect(catalog).toContain("refunds"); + }); + + it("handles multiple skills", () => { + const skills: SkillSummary[] = [ + { name: "code-review", skillPath: "code-review", uri: "skill://code-review/SKILL.md", description: "Review code" }, + { name: "refunds", skillPath: "acme/billing/refunds", uri: "skill://acme/billing/refunds/SKILL.md", description: "Process refunds" }, + ]; + const catalog = buildSkillsCatalog(skills, catalogOptions); + expect(catalog).toContain("code-review"); + expect(catalog).toContain("refunds"); + }); + + it("escapes XML special characters", () => { + const skills: SkillSummary[] = [ + { name: "test&skill", skillPath: "test&skill", uri: "skill://test&skill/SKILL.md", description: "Uses & ampersands" }, + ]; + const catalog = buildSkillsCatalog(skills, catalogOptions); + expect(catalog).toContain("&"); + expect(catalog).toContain("<brackets>"); + expect(catalog).not.toContain(""); + }); + + it("works with non-skill:// URI schemes", () => { + const skills: SkillSummary[] = [ + { name: "copilot-sdk", skillPath: "copilot-sdk", uri: "repo://github/awesome-copilot/contents/skills/copilot-sdk/SKILL.md", description: "Copilot SDK" }, + ]; + const catalog = buildSkillsCatalog(skills, catalogOptions); + expect(catalog).toContain("repo://github/awesome-copilot/contents/skills/copilot-sdk/SKILL.md"); + expect(catalog).toContain("copilot-sdk"); + }); +}); diff --git a/typescript/sdk/src/client/index.ts b/typescript/sdk/src/client/index.ts new file mode 100644 index 0000000..397bc17 --- /dev/null +++ b/typescript/sdk/src/client/index.ts @@ -0,0 +1,39 @@ +/** + * Client-side exports for the Skills Extension SDK. + */ + +export { + READ_RESOURCE_TOOL, + READ_SKILL_TOOL, + discoverSkills, + discoverAndBuildCatalog, + listSkills, + listSkillsFromIndex, + listSkillsFromInstructions, + extractSkillUrisFromInstructions, + readSkillUri, + readSkillContent, + readSkillArchive, + parseSkillFrontmatter, + buildSkillsSummary, + buildSkillsCatalog, + readSkillDocument, + verifyDigest, + readSkillUriVerified, + serverSupportsDirectoryRead, + readDirectory, + walkDirectory, +} from "../_client.js"; +export type { SkillsClient, ToolDefinition } from "../_client.js"; +export type { + SkillSummary, + SkillArchiveRef, + SkillsCatalogOptions, + DiscoverSkillsOptions, + DiscoverCatalogOptions, + DiscoverCatalogResult, + InstructionsUriExtractor, + UnpackedSkillArchive, + ExtractArchiveOptions, +} from "../types.js"; +export type { DirectoryChild, DirectoryReadResult } from "../directory.js"; diff --git a/typescript/sdk/src/directory.test.ts b/typescript/sdk/src/directory.test.ts new file mode 100644 index 0000000..d145226 --- /dev/null +++ b/typescript/sdk/src/directory.test.ts @@ -0,0 +1,404 @@ +/** + * Tests for the SEP-2640 `resources/directory/read` module, digest + * verification, the `directoryRead` capability declaration, and the + * client-side directory helpers. + */ + +import { describe, it, expect, vi } from "vitest"; +import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; +import { + buildDirectoryTree, + makeDirectoryReadHandler, + INODE_DIRECTORY_MIME, + DIRECTORY_READ_METHOD, +} from "./directory.js"; +import { + declareSkillsExtension, + SKILLS_EXTENSION_ID, +} from "./resource-extensions.js"; +import { + verifyDigest, + serverSupportsDirectoryRead, + readDirectory, + walkDirectory, + readSkillUri, + type SkillsClient, +} from "./_client.js"; +import { sha256Digest } from "./_server.js"; +import type { SkillMetadata } from "./types.js"; + +function skill(overrides: Partial & { + name: string; + skillPath: string; +}): SkillMetadata { + return { + description: "desc", + absolutePath: `/skills/${overrides.skillPath}/SKILL.md`, + skillDir: `/skills/${overrides.skillPath}`, + documents: [], + size: 42, + lastModified: "2026-01-01T00:00:00.000Z", + frontmatter: { name: overrides.name, description: "desc" }, + digest: "sha256:" + "0".repeat(64), + ...overrides, + }; +} + +function skillMap(skills: SkillMetadata[]): Map { + return new Map(skills.map((s) => [s.skillPath, s])); +} + +// --------------------------------------------------------------------------- +// buildDirectoryTree +// --------------------------------------------------------------------------- + +describe("buildDirectoryTree", () => { + it("lists the skill root's direct children (files + subdirs)", () => { + const tree = buildDirectoryTree( + skillMap([ + skill({ + name: "code-review", + skillPath: "code-review", + documents: [ + { path: "references/GUIDE.md", mimeType: "text/markdown", size: 10 }, + { path: "scripts/run.sh", mimeType: "text/x-shellscript", size: 5 }, + ], + }), + ]), + ); + + const root = tree.get("skill://code-review")!; + expect(root.map((c) => c.name).sort()).toEqual([ + "SKILL.md", + "references", + "scripts", + ]); + // Directories carry inode/directory; files carry their own mime. + expect(root.find((c) => c.name === "references")!.mimeType).toBe(INODE_DIRECTORY_MIME); + expect(root.find((c) => c.name === "SKILL.md")!.mimeType).toBe("text/markdown"); + }); + + it("uses no trailing slash on directory URIs and lists subdirectory contents", () => { + const tree = buildDirectoryTree( + skillMap([ + skill({ + name: "code-review", + skillPath: "code-review", + documents: [{ path: "references/GUIDE.md", mimeType: "text/markdown", size: 10 }], + }), + ]), + ); + + expect(tree.has("skill://code-review/references")).toBe(true); + expect(tree.has("skill://code-review/references/")).toBe(false); + const refs = tree.get("skill://code-review/references")!; + expect(refs).toHaveLength(1); + expect(refs[0].uri).toBe("skill://code-review/references/GUIDE.md"); + }); + + it("exposes organizational prefix segments as directories", () => { + const tree = buildDirectoryTree( + skillMap([skill({ name: "refunds", skillPath: "acme/billing/refunds" })]), + ); + + expect(tree.get("skill://acme")!.map((c) => c.name)).toEqual(["billing"]); + expect(tree.get("skill://acme/billing")!.map((c) => c.name)).toEqual(["refunds"]); + expect(tree.get("skill://acme/billing/refunds")!.map((c) => c.name)).toEqual(["SKILL.md"]); + // No synthetic root. + expect(tree.has("skill://")).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// makeDirectoryReadHandler +// --------------------------------------------------------------------------- + +describe("makeDirectoryReadHandler", () => { + const map = skillMap([ + skill({ + name: "code-review", + skillPath: "code-review", + documents: [{ path: "references/GUIDE.md", mimeType: "text/markdown", size: 10 }], + }), + ]); + + function call(uri: string, cursor?: string, pageSize?: number) { + const handler = makeDirectoryReadHandler(map, pageSize ? { pageSize } : undefined); + return handler({ + method: DIRECTORY_READ_METHOD, + params: { uri, ...(cursor ? { cursor } : {}) }, + } as never); + } + + it("returns direct children, metadata only (no contents)", async () => { + const result = await call("skill://code-review"); + expect(result.resources.map((r) => r.name).sort()).toEqual(["SKILL.md", "references"]); + for (const child of result.resources) { + expect("text" in child).toBe(false); + expect("blob" in child).toBe(false); + } + }); + + it("is non-recursive (grandchildren absent)", async () => { + const result = await call("skill://code-review"); + expect(result.resources.find((r) => r.name === "GUIDE.md")).toBeUndefined(); + }); + + it("tolerates a trailing slash on the requested URI", async () => { + const result = await call("skill://code-review/"); + expect(result.resources).toHaveLength(2); + }); + + it("throws -32602 for a file URI", async () => { + await expect(call("skill://code-review/SKILL.md")).rejects.toMatchObject({ + code: ErrorCode.InvalidParams, + }); + }); + + it("throws -32602 for a nonexistent URI", async () => { + const err = await call("skill://does-not-exist").catch((e) => e); + expect(err).toBeInstanceOf(McpError); + expect(err.code).toBe(-32602); + }); + + it("paginates with an opaque nextCursor", async () => { + const refsMap = skillMap([ + skill({ + name: "many", + skillPath: "many", + documents: [ + { path: "a.md", mimeType: "text/markdown", size: 1 }, + { path: "b.md", mimeType: "text/markdown", size: 1 }, + { path: "c.md", mimeType: "text/markdown", size: 1 }, + ], + }), + ]); + const handler = makeDirectoryReadHandler(refsMap, { pageSize: 2 }); + + const page1 = await handler({ + method: DIRECTORY_READ_METHOD, + params: { uri: "skill://many" }, + } as never); + expect(page1.resources).toHaveLength(2); + expect(page1.nextCursor).toBeTypeOf("string"); + + const page2 = await handler({ + method: DIRECTORY_READ_METHOD, + params: { uri: "skill://many", cursor: page1.nextCursor }, + } as never); + // Root has SKILL.md + a.md + b.md + c.md = 4 children → 2 + 2, no more. + expect(page2.resources).toHaveLength(2); + expect(page2.nextCursor).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// verifyDigest +// --------------------------------------------------------------------------- + +describe("verifyDigest", () => { + const data = "hello skills"; + const digest = sha256Digest(data); + + it("returns true for matching content", () => { + expect(verifyDigest(data, digest)).toBe(true); + }); + + it("returns false for mismatched content", () => { + expect(verifyDigest("tampered", digest)).toBe(false); + }); + + it("is case-insensitive on the hex", () => { + expect(verifyDigest(data, digest.toUpperCase())).toBe(true); + }); + + it("throws (rather than returning false) on a malformed expected digest", () => { + expect(() => verifyDigest(data, "not-a-digest")).toThrow(/Malformed digest/); + expect(() => verifyDigest(data, "sha256:abc")).toThrow(/Malformed digest/); + expect(() => verifyDigest(data, "sha256:")).toThrow(/Malformed digest/); + expect(() => verifyDigest(data, "md5:" + "0".repeat(32))).toThrow( + /Malformed digest/, + ); + }); +}); + +// --------------------------------------------------------------------------- +// declareSkillsExtension +// --------------------------------------------------------------------------- + +describe("declareSkillsExtension", () => { + function stubServer() { + const registered: unknown[] = []; + return { + registered, + registerCapabilities(caps: unknown) { + registered.push(caps); + }, + }; + } + + it("declares an empty capability object by default", () => { + const server = stubServer(); + declareSkillsExtension(server); + expect(server.registered[0]).toEqual({ + extensions: { [SKILLS_EXTENSION_ID]: {} }, + }); + }); + + it("declares directoryRead when requested", () => { + const server = stubServer(); + declareSkillsExtension(server, { directoryRead: true }); + expect(server.registered[0]).toEqual({ + extensions: { [SKILLS_EXTENSION_ID]: { directoryRead: true } }, + }); + }); +}); + +// --------------------------------------------------------------------------- +// serverSupportsDirectoryRead / readDirectory (client) +// --------------------------------------------------------------------------- + +describe("client directory helpers", () => { + function clientWithCaps( + cap: { directoryRead?: boolean } | undefined, + request?: SkillsClient["request"], + ): SkillsClient { + return { + listResources: vi.fn(), + readResource: vi.fn(), + getServerCapabilities: () => ({ + extensions: cap ? { [SKILLS_EXTENSION_ID]: cap } : {}, + }), + request, + }; + } + + it("serverSupportsDirectoryRead reflects the declared capability", () => { + expect(serverSupportsDirectoryRead(clientWithCaps({ directoryRead: true }))).toBe(true); + expect(serverSupportsDirectoryRead(clientWithCaps({ directoryRead: false }))).toBe(false); + expect(serverSupportsDirectoryRead(clientWithCaps(undefined))).toBe(false); + expect(serverSupportsDirectoryRead({ listResources: vi.fn(), readResource: vi.fn() })).toBe(false); + }); + + it("readDirectory throws when the capability is absent", async () => { + await expect( + readDirectory(clientWithCaps({ directoryRead: false }), "skill://x"), + ).rejects.toThrow(/did not declare/); + }); + + it("readDirectory issues the correct low-level request when supported", async () => { + const request = vi.fn().mockResolvedValue({ + resources: [{ uri: "skill://x/SKILL.md", name: "SKILL.md", mimeType: "text/markdown" }], + }); + const client = clientWithCaps({ directoryRead: true }, request); + + const result = await readDirectory(client, "skill://x"); + + expect(request).toHaveBeenCalledWith( + { method: DIRECTORY_READ_METHOD, params: { uri: "skill://x" } }, + expect.anything(), + ); + expect(result.resources[0].name).toBe("SKILL.md"); + }); + + it("readDirectory forwards a pagination cursor", async () => { + const request = vi.fn().mockResolvedValue({ resources: [] }); + const client = clientWithCaps({ directoryRead: true }, request); + + await readDirectory(client, "skill://x", { cursor: "abc" }); + + expect(request).toHaveBeenCalledWith( + { method: DIRECTORY_READ_METHOD, params: { uri: "skill://x", cursor: "abc" } }, + expect.anything(), + ); + }); + + it("walkDirectory descends subdirectories and collects only files", async () => { + const request = vi.fn(async (req: { params: { uri: string } }) => { + if (req.params.uri === "skill://x") + return { + resources: [ + { uri: "skill://x/SKILL.md", name: "SKILL.md", mimeType: "text/markdown" }, + { uri: "skill://x/refs", name: "refs", mimeType: INODE_DIRECTORY_MIME }, + ], + }; + return { + resources: [ + { uri: "skill://x/refs/GUIDE.md", name: "GUIDE.md", mimeType: "text/markdown" }, + ], + }; + }); + const client = clientWithCaps({ directoryRead: true }, request as never); + + const files = await walkDirectory(client, "skill://x"); + expect(files.map((f) => f.uri).sort()).toEqual([ + "skill://x/SKILL.md", + "skill://x/refs/GUIDE.md", + ]); + }); + + it("walkDirectory bails out instead of looping on a non-advancing cursor", async () => { + // A misbehaving server that always returns the same nextCursor would + // otherwise spin forever; the helper must detect the repeat and throw. + const request = vi.fn().mockResolvedValue({ + resources: [{ uri: "skill://x/a.md", name: "a.md", mimeType: "text/markdown" }], + nextCursor: "stuck", + }); + const client = clientWithCaps({ directoryRead: true }, request); + + await expect(walkDirectory(client, "skill://x")).rejects.toThrow( + /did not advance/, + ); + }); + + it("walkDirectory does not re-enter a directory it already visited (cycle guard)", async () => { + // Two directories that list each other would loop forever without a + // visited set. The walk must terminate and visit each dir once. + const request = vi.fn(async (req: { params: { uri: string } }) => { + if (req.params.uri === "skill://x") + return { + resources: [{ uri: "skill://y", name: "y", mimeType: INODE_DIRECTORY_MIME }], + }; + return { + resources: [{ uri: "skill://x", name: "x", mimeType: INODE_DIRECTORY_MIME }], + }; + }); + const client = clientWithCaps({ directoryRead: true }, request as never); + + const files = await walkDirectory(client, "skill://x"); + expect(files).toEqual([]); + // skill://x and skill://y each read exactly once. + expect(request).toHaveBeenCalledTimes(2); + }); +}); + +// --------------------------------------------------------------------------- +// readSkillUri +// --------------------------------------------------------------------------- + +describe("readSkillUri", () => { + function clientReturning(content: unknown): SkillsClient { + return { + listResources: vi.fn(), + readResource: vi.fn().mockResolvedValue({ contents: [content] }), + }; + } + + it("returns the text content of a resource", async () => { + const text = await readSkillUri(clientReturning({ text: "# Hello" }), "skill://x/SKILL.md"); + expect(text).toBe("# Hello"); + }); + + it("returns an empty string for a legitimately empty text resource", async () => { + // Regression: the guard previously tested the falsy `text` value, so an + // empty (zero-byte) file was rejected as "Expected text content". + const text = await readSkillUri(clientReturning({ text: "" }), "skill://x/empty.txt"); + expect(text).toBe(""); + }); + + it("throws when the content carries no text field", async () => { + await expect( + readSkillUri(clientReturning({ blob: "AAAA" }), "skill://x/img.png"), + ).rejects.toThrow(/Expected text content/); + }); +}); diff --git a/typescript/sdk/src/directory.ts b/typescript/sdk/src/directory.ts new file mode 100644 index 0000000..1212382 --- /dev/null +++ b/typescript/sdk/src/directory.ts @@ -0,0 +1,240 @@ +/** + * `resources/directory/read` — directory enumeration for skills served as + * individual files (SEP-2640). + * + * A skill is a directory of files. Hosts that materialize a skill (or walk + * its contents) need to enumerate the files under a skill root without + * already knowing every URI. SEP-2640 adds a dedicated method for this: + * + * request: { method: "resources/directory/read", params: { uri, cursor? } } + * result: { resources: Resource[], nextCursor? } // same shape as resources/list + * + * The listing is metadata-only (each child's `uri`/`name`/`mimeType`, NOT its + * contents), non-recursive (clients descend by calling again on a child + * directory), and cursor-paginated. A directory resource is one whose + * `mimeType` is `inode/directory`; directory URIs are written without a + * trailing slash. Reading a non-directory or nonexistent URI is an error + * (`-32602` Invalid params). + * + * Archive-distributed skills are opaque to the server (the archive isn't + * unpacked at registration), so directory read only covers skills served as + * individual files. + */ + +import { z } from "zod"; +import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; +import type { SkillMetadata } from "./types.js"; +import { getMimeType } from "./mime.js"; +import { SKILL_URI_SCHEME } from "./uri.js"; + +/** JSON-RPC method name for directory enumeration (SEP-2640). */ +export const DIRECTORY_READ_METHOD = "resources/directory/read"; + +/** The `mimeType` that marks a resource as a directory (SEP-2640). */ +export const INODE_DIRECTORY_MIME = "inode/directory"; + +/** Default page size for a `resources/directory/read` response. */ +export const DEFAULT_DIRECTORY_PAGE_SIZE = 100; + +/** + * Request schema for `resources/directory/read`. The `method` literal is how + * the MCP SDK's low-level `Server.setRequestHandler` routes the call. + */ +export const DirectoryReadRequestSchema = z.object({ + method: z.literal(DIRECTORY_READ_METHOD), + params: z.object({ + uri: z.string(), + cursor: z.string().optional(), + }), +}); + +/** + * Result schema for `resources/directory/read`, mirroring the + * `resources/list` result shape (`{ resources, nextCursor? }`). Passed to the + * client's low-level `request()` so the response is validated. Unknown fields + * pass through. + */ +export const DirectoryReadResultSchema = z + .object({ + resources: z.array( + z + .object({ + uri: z.string(), + name: z.string(), + title: z.string().optional(), + mimeType: z.string().optional(), + description: z.string().optional(), + size: z.number().optional(), + }) + .passthrough(), + ), + nextCursor: z.string().optional(), + }) + .passthrough(); + +/** + * A child resource in a directory listing — a structural subset of the MCP + * `Resource` type (metadata only). Directories carry `inode/directory`. + */ +export interface DirectoryChild { + uri: string; + name: string; + mimeType: string; + /** File size in bytes, when known. Omitted for directories. */ + size?: number; +} + +/** Result shape returned by the `resources/directory/read` handler. */ +export interface DirectoryReadResult { + resources: DirectoryChild[]; + nextCursor?: string; +} + +/** Options for the directory-read handler. */ +export interface DirectoryReadHandlerOptions { + /** Children per page. Default {@link DEFAULT_DIRECTORY_PAGE_SIZE}. */ + pageSize?: number; +} + +/** Strip a single trailing slash (but never reduce the scheme itself). */ +function stripTrailingSlash(uri: string): string { + if (uri.length > SKILL_URI_SCHEME.length && uri.endsWith("/")) { + return uri.slice(0, -1); + } + return uri; +} + +/** + * Build the directory tree implied by a skill map. + * + * Every directory reachable from a served file — the skill root + * (`skill://`), each organizational prefix segment + * (`skill://acme`, `skill://acme/billing`), and any subdirectory holding a + * supporting document — becomes a key whose value is its **direct** children + * (files and immediate subdirectories). No synthetic `skill://` root is + * invented. + * + * @returns Map keyed by directory URI (no trailing slash) → sorted children. + */ +export function buildDirectoryTree( + skillMap: Map, +): Map { + // dirPath (without scheme) → childName → child descriptor + const dirs = new Map>(); + + const ensureDir = (dirPath: string): Map => { + let d = dirs.get(dirPath); + if (!d) { + d = new Map(); + dirs.set(dirPath, d); + } + return d; + }; + + /** Record a file at `/`, creating ancestors. */ + const addFile = (segments: string[], size?: number) => { + // segments includes the file name as its last element. + for (let i = 0; i < segments.length; i++) { + const childName = segments[i]; + const parentPath = segments.slice(0, i).join("/"); + const isLast = i === segments.length - 1; + const dir = ensureDir(parentPath); + const childPath = segments.slice(0, i + 1).join("/"); + if (isLast) { + dir.set(childName, { + uri: `${SKILL_URI_SCHEME}${childPath}`, + name: childName, + mimeType: getMimeType(childName), + ...(size !== undefined ? { size } : {}), + }); + } else { + // Intermediate segment: a subdirectory. Don't clobber a file. + if (!dir.has(childName)) { + dir.set(childName, { + uri: `${SKILL_URI_SCHEME}${childPath}`, + name: childName, + mimeType: INODE_DIRECTORY_MIME, + }); + } + } + } + }; + + for (const [skillPath, skill] of skillMap) { + const base = skillPath.split("/"); + // SKILL.md at the skill root. + addFile([...base, "SKILL.md"], skill.size); + // Supporting documents, addressed relative to the skill root. + for (const doc of skill.documents) { + addFile([...base, ...doc.path.split("/")], doc.size); + } + } + + // Materialize, dropping the synthetic empty-string root and sorting children + // (directories first, then files, each alphabetically) for stable paging. + const out = new Map(); + for (const [dirPath, children] of dirs) { + if (dirPath === "") continue; + const list = Array.from(children.values()).sort((a, b) => { + const aDir = a.mimeType === INODE_DIRECTORY_MIME; + const bDir = b.mimeType === INODE_DIRECTORY_MIME; + if (aDir !== bDir) return aDir ? -1 : 1; + return a.name.localeCompare(b.name); + }); + out.set(`${SKILL_URI_SCHEME}${dirPath}`, list); + } + return out; +} + +/** Decode an opaque pagination cursor to a numeric offset. */ +function decodeCursor(cursor: string | undefined): number { + if (!cursor) return 0; + try { + const n = parseInt(Buffer.from(cursor, "base64").toString("utf-8"), 10); + return Number.isInteger(n) && n >= 0 ? n : 0; + } catch { + return 0; + } +} + +/** Encode a numeric offset as an opaque pagination cursor. */ +function encodeCursor(offset: number): string { + return Buffer.from(String(offset), "utf-8").toString("base64"); +} + +/** + * Build a `resources/directory/read` handler backed by an in-memory skill + * map. The returned function plugs directly into the MCP SDK's low-level + * `Server.setRequestHandler(DirectoryReadRequestSchema, handler)`. + * + * Throws `McpError(InvalidParams)` (`-32602`) when the requested URI is not a + * known directory (i.e. it is a file, or does not exist). + */ +export function makeDirectoryReadHandler( + skillMap: Map, + options?: DirectoryReadHandlerOptions, +): (request: z.infer) => Promise { + const tree = buildDirectoryTree(skillMap); + const pageSize = options?.pageSize ?? DEFAULT_DIRECTORY_PAGE_SIZE; + + return async (request) => { + const uri = stripTrailingSlash(request.params.uri); + const children = tree.get(uri); + if (children === undefined) { + throw new McpError( + ErrorCode.InvalidParams, + `Not a directory resource or does not exist: ${request.params.uri}`, + ); + } + + const offset = decodeCursor(request.params.cursor); + const page = children.slice(offset, offset + pageSize); + const nextOffset = offset + page.length; + const hasMore = nextOffset < children.length; + + return { + resources: page, + ...(hasMore ? { nextCursor: encodeCursor(nextOffset) } : {}), + }; + }; +} diff --git a/typescript/sdk/src/discover.test.ts b/typescript/sdk/src/discover.test.ts new file mode 100644 index 0000000..1c7b816 --- /dev/null +++ b/typescript/sdk/src/discover.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { discoverSkills } from "./_server.js"; + +// --------------------------------------------------------------------------- +// discoverSkills (filesystem) — frontmatter parsing +// --------------------------------------------------------------------------- + +describe("discoverSkills frontmatter parsing", () => { + let tmpDir: string; + + beforeAll(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ext-skills-discover-")); + }); + afterAll(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function writeSkill(name: string, skillMd: string): void { + const dir = path.join(tmpDir, name); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, "SKILL.md"), skillMd); + } + + it("does not let a '---' inside the frontmatter terminate it early", () => { + // A literal block scalar whose indented content includes a `---` line. + // A naive content.split('---') truncates `description` at that line and + // loses everything after it; a line-anchored parse keeps the whole value. + const skillMd = [ + "---", + "name: demo", + "description: |", + " First line of the description.", + " ---", + " Line after a rule inside the block scalar.", + "---", + "", + "# Body", + "", + "Some prose, then a real horizontal rule:", + "", + "---", + "", + "Text below the rule.", + "", + ].join("\n"); + writeSkill("demo", skillMd); + + const map = discoverSkills(tmpDir); + const skill = map.get("demo"); + + expect(skill).toBeDefined(); + expect(skill!.description).toContain( + "Line after a rule inside the block scalar.", + ); + }); +}); diff --git a/typescript/sdk/src/index-json.test.ts b/typescript/sdk/src/index-json.test.ts new file mode 100644 index 0000000..4a40cca --- /dev/null +++ b/typescript/sdk/src/index-json.test.ts @@ -0,0 +1,791 @@ +import { describe, it, expect, vi, beforeAll, afterAll } from "vitest"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import { generateSkillIndex, sha256Digest } from "./_server.js"; +import { + listSkillsFromIndex, + listSkills, + discoverSkills, + discoverAndBuildCatalog, +} from "./_client.js"; +import type { SkillMetadata } from "./types.js"; +import type { SkillsClient } from "./_client.js"; + +const SHA256_RE = /^sha256:[0-9a-f]{64}$/; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function makeSkill( + overrides: Partial & { + name: string; + skillPath: string; + description: string; + }, +): SkillMetadata { + return { + absolutePath: `/skills/${overrides.skillPath}/SKILL.md`, + skillDir: `/skills/${overrides.skillPath}`, + documents: [], + size: 0, + lastModified: "2026-01-01T00:00:00.000Z", + frontmatter: { name: overrides.name, description: overrides.description }, + digest: "sha256:" + "0".repeat(64), + ...overrides, + }; +} + +function makeSkillMap(skills: SkillMetadata[]): Map { + return new Map(skills.map((s) => [s.skillPath, s])); +} + +/** A type-less skill-md index entry (SEP-2640). */ +function skillMdEntry( + name: string, + url: string, + description: string, + extraFrontmatter: Record = {}, +) { + return { + frontmatter: { name, description, ...extraFrontmatter }, + url, + digest: "sha256:" + "a".repeat(64), + }; +} + +/** A type-less archive-only index entry (SEP-2640). */ +function archiveEntry( + name: string, + archiveUrl: string, + description: string, + mimeType = "application/gzip", +) { + return { + frontmatter: { name, description }, + archives: [{ url: archiveUrl, mimeType, digest: "sha256:" + "b".repeat(64) }], + }; +} + +/** Create a mock client that returns the given index JSON from readResource. */ +function mockClientWithIndex(indexJson: unknown): SkillsClient { + return { + listResources: vi.fn().mockResolvedValue({ resources: [] }), + readResource: vi.fn().mockResolvedValue({ + contents: [{ text: JSON.stringify(indexJson) }], + }), + }; +} + +// Real temp archive files (the index reader hashes archive bytes for `digest`). +let tmpDir: string; +const archivePath = (name: string) => path.join(tmpDir, name); +function writeArchive(name: string, bytes = `bytes-of-${name}`): string { + const p = archivePath(name); + fs.writeFileSync(p, bytes); + return p; +} + +beforeAll(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "ext-skills-index-")); +}); +afterAll(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +// --------------------------------------------------------------------------- +// generateSkillIndex (server-side) +// --------------------------------------------------------------------------- + +describe("generateSkillIndex", () => { + it("generates correct index for single skill", () => { + const map = makeSkillMap([ + makeSkill({ name: "code-review", skillPath: "code-review", description: "Review code" }), + ]); + + const index = generateSkillIndex(map); + + expect("$schema" in index).toBe(false); + expect(index.skills).toHaveLength(1); + expect(index.skills[0]).toEqual({ + frontmatter: { name: "code-review", description: "Review code" }, + url: "skill://code-review/SKILL.md", + digest: "sha256:" + "0".repeat(64), + }); + }); + + it("copies the full frontmatter block verbatim", () => { + const map = makeSkillMap([ + makeSkill({ + name: "refunds", + skillPath: "refunds", + description: "Refunds", + frontmatter: { + name: "refunds", + description: "Refunds", + license: "Apache-2.0", + metadata: { team: "billing" }, + }, + }), + ]); + + const index = generateSkillIndex(map); + expect(index.skills[0].frontmatter).toEqual({ + name: "refunds", + description: "Refunds", + license: "Apache-2.0", + metadata: { team: "billing" }, + }); + }); + + it("generates correct URIs for multi-segment paths", () => { + const map = makeSkillMap([ + makeSkill({ name: "refunds", skillPath: "acme/billing/refunds", description: "Process refunds" }), + makeSkill({ name: "onboarding", skillPath: "acme/onboarding", description: "Onboard employees" }), + ]); + + const index = generateSkillIndex(map); + + expect(index.skills).toHaveLength(2); + expect(index.skills[0].url).toBe("skill://acme/billing/refunds/SKILL.md"); + expect(index.skills[0].frontmatter.name).toBe("refunds"); + expect(index.skills[1].url).toBe("skill://acme/onboarding/SKILL.md"); + expect(index.skills[1].frontmatter.name).toBe("onboarding"); + }); + + it("returns empty skills array for empty map", () => { + const index = generateSkillIndex(new Map()); + expect("$schema" in index).toBe(false); + expect(index.skills).toEqual([]); + }); + + it("every entry carries frontmatter, url, and a sha256 digest", () => { + const map = makeSkillMap([ + makeSkill({ name: "a", skillPath: "a", description: "A" }), + makeSkill({ name: "b", skillPath: "x/b", description: "B" }), + ]); + + const index = generateSkillIndex(map); + for (const entry of index.skills) { + expect(entry.frontmatter).toBeTypeOf("object"); + expect(typeof entry.url).toBe("string"); + expect(entry.digest).toMatch(SHA256_RE); + expect("type" in entry).toBe(false); + } + }); +}); + +// --------------------------------------------------------------------------- +// listSkillsFromIndex (client-side) +// --------------------------------------------------------------------------- + +describe("listSkillsFromIndex", () => { + it("parses a valid index into SkillSummary array", async () => { + const client = mockClientWithIndex({ + skills: [ + skillMdEntry("code-review", "skill://code-review/SKILL.md", "Review code"), + skillMdEntry("refunds", "skill://acme/billing/refunds/SKILL.md", "Refunds"), + ], + }); + + const skills = await listSkillsFromIndex(client); + + expect(skills).toHaveLength(2); + expect(skills![0]).toMatchObject({ + name: "code-review", + skillPath: "code-review", + uri: "skill://code-review/SKILL.md", + type: "skill-md", + description: "Review code", + mimeType: "text/markdown", + digest: "sha256:" + "a".repeat(64), + }); + expect(skills![1]).toMatchObject({ + name: "refunds", + skillPath: "acme/billing/refunds", + uri: "skill://acme/billing/refunds/SKILL.md", + type: "skill-md", + description: "Refunds", + }); + }); + + it("skips entries with neither url nor archives", async () => { + const client = mockClientWithIndex({ + skills: [ + skillMdEntry("good", "skill://good/SKILL.md", "Good"), + { frontmatter: { name: "orphan", description: "No way to fetch me" } }, + ], + }); + + const skills = await listSkillsFromIndex(client); + expect(skills).toHaveLength(1); + expect(skills![0].name).toBe("good"); + }); + + it("returns null when server throws (no index.json)", async () => { + const client: SkillsClient = { + listResources: vi.fn(), + readResource: vi.fn().mockRejectedValue(new Error("Resource not found")), + }; + + const skills = await listSkillsFromIndex(client); + expect(skills).toBeNull(); + }); + + it("returns null for empty content", async () => { + const client: SkillsClient = { + listResources: vi.fn(), + readResource: vi.fn().mockResolvedValue({ contents: [] }), + }; + + const skills = await listSkillsFromIndex(client); + expect(skills).toBeNull(); + }); + + it("returns null for malformed JSON (missing skills array)", async () => { + const client = mockClientWithIndex({}); + const skills = await listSkillsFromIndex(client); + expect(skills).toBeNull(); + }); + + it("reads from the correct well-known URI", async () => { + const readResource = vi.fn().mockResolvedValue({ + contents: [{ text: JSON.stringify({ skills: [] }) }], + }); + const client: SkillsClient = { listResources: vi.fn(), readResource }; + + await listSkillsFromIndex(client); + + expect(readResource).toHaveBeenCalledWith({ uri: "skill://index.json" }); + }); +}); + +// --------------------------------------------------------------------------- +// generateSkillIndex with archive declarations +// --------------------------------------------------------------------------- + +describe("generateSkillIndex with archives", () => { + it("emits a per-skill archives array with url, mimeType, and digest", () => { + const p = writeArchive("pdf-processing.tar.gz"); + const index = generateSkillIndex(new Map(), { + archives: [ + { + name: "pdf-processing", + description: "Extract and assemble PDFs", + skillPath: "pdf-processing", + archivePath: p, + }, + ], + }); + + expect(index.skills).toHaveLength(1); + expect(index.skills[0].frontmatter).toEqual({ + name: "pdf-processing", + description: "Extract and assemble PDFs", + }); + expect(index.skills[0].url).toBeUndefined(); + expect(index.skills[0].archives).toHaveLength(1); + expect(index.skills[0].archives![0]).toEqual({ + url: "skill://pdf-processing.tar.gz", + mimeType: "application/gzip", + digest: sha256Digest(fs.readFileSync(p)), + }); + }); + + it("uses the declaration's verbatim frontmatter when provided", () => { + const p = writeArchive("refunds.tar.gz"); + const index = generateSkillIndex(new Map(), { + archives: [ + { + name: "refunds", + description: "Refunds", + skillPath: "refunds", + archivePath: p, + frontmatter: { name: "refunds", description: "Refunds", license: "MIT" }, + }, + ], + }); + expect(index.skills[0].frontmatter).toEqual({ + name: "refunds", + description: "Refunds", + license: "MIT", + }); + }); + + it("derives URL suffix and mimeType from archivePath extension", () => { + const px = writeArchive("x.zip"); + const py = writeArchive("y.tgz"); + const index = generateSkillIndex(new Map(), { + archives: [ + { name: "x", description: "X", skillPath: "x", archivePath: px }, + { name: "y", description: "Y", skillPath: "y", archivePath: py }, + ], + }); + + expect(index.skills[0].archives![0].url).toBe("skill://x.zip"); + expect(index.skills[0].archives![0].mimeType).toBe("application/zip"); + expect(index.skills[1].archives![0].url).toBe("skill://y.tar.gz"); + expect(index.skills[1].archives![0].mimeType).toBe("application/gzip"); + }); + + it("respects explicit format override", () => { + const p = writeArchive("x.bundle"); + const index = generateSkillIndex(new Map(), { + archives: [ + { name: "x", description: "X", skillPath: "x", archivePath: p, format: "zip" }, + ], + }); + + expect(index.skills[0].archives![0].url).toBe("skill://x.zip"); + expect(index.skills[0].archives![0].mimeType).toBe("application/zip"); + }); + + it("preserves multi-segment skillPath in the archive URL", () => { + const p = writeArchive("refunds-multi.tar.gz"); + const index = generateSkillIndex(new Map(), { + archives: [ + { + name: "refunds", + description: "Refunds", + skillPath: "acme/billing/refunds", + archivePath: p, + }, + ], + }); + + expect(index.skills[0].archives![0].url).toBe( + "skill://acme/billing/refunds.tar.gz", + ); + }); + + it("rejects archive whose skillPath final segment != name", () => { + expect(() => + generateSkillIndex(new Map(), { + archives: [ + { + name: "wrong-name", + description: "X", + skillPath: "acme/billing/refunds", + archivePath: archivePath("never-read.tar.gz"), + }, + ], + }), + ).toThrow(/final segment "refunds" does not match name "wrong-name"/); + }); + + it("emits both skill-md and archive entries in one index", () => { + const p = writeArchive("b.tar.gz"); + const map = makeSkillMap([ + makeSkill({ name: "a", skillPath: "a", description: "A" }), + ]); + + const index = generateSkillIndex(map, { + archives: [ + { name: "b", description: "B", skillPath: "b", archivePath: p }, + ], + }); + + expect(index.skills).toHaveLength(2); + // skill-md entry: url + digest, no archives + expect(index.skills[0].url).toBe("skill://a/SKILL.md"); + expect(index.skills[0].archives).toBeUndefined(); + // archive entry: archives, no url + expect(index.skills[1].url).toBeUndefined(); + expect(index.skills[1].archives).toHaveLength(1); + }); +}); + +// --------------------------------------------------------------------------- +// listSkillsFromIndex with archive entries +// --------------------------------------------------------------------------- + +describe("listSkillsFromIndex with archives", () => { + it("returns archive entries with type set", async () => { + const client = mockClientWithIndex({ + skills: [archiveEntry("pdf-processing", "skill://pdf-processing.tar.gz", "PDFs")], + }); + + const skills = await listSkillsFromIndex(client); + expect(skills).toHaveLength(1); + expect(skills![0]).toMatchObject({ + name: "pdf-processing", + skillPath: "pdf-processing", + uri: "skill://pdf-processing.tar.gz", + type: "archive", + description: "PDFs", + mimeType: "application/gzip", + digest: "sha256:" + "b".repeat(64), + }); + expect(skills![0].archives).toHaveLength(1); + }); + + it("derives skillPath by stripping archive suffix", async () => { + const client = mockClientWithIndex({ + skills: [ + archiveEntry( + "refunds", + "skill://acme/billing/refunds.zip", + "Refunds", + "application/zip", + ), + ], + }); + + const skills = await listSkillsFromIndex(client); + expect(skills![0].skillPath).toBe("acme/billing/refunds"); + expect(skills![0].mimeType).toBe("application/zip"); + }); + + it("returns mixed skill-md and archive entries", async () => { + const client = mockClientWithIndex({ + skills: [ + skillMdEntry("a", "skill://a/SKILL.md", "A"), + archiveEntry("b", "skill://b.tar.gz", "B"), + ], + }); + + const skills = await listSkillsFromIndex(client); + expect(skills).toHaveLength(2); + expect(skills!.map((s) => s.type)).toEqual(["skill-md", "archive"]); + }); +}); + +// --------------------------------------------------------------------------- +// listSkillsFromIndex with non-skill:// URI schemes +// --------------------------------------------------------------------------- + +describe("listSkillsFromIndex with non-skill:// URI schemes", () => { + it("handles entries with any URI scheme", async () => { + const client = mockClientWithIndex({ + skills: [ + skillMdEntry( + "copilot-sdk", + "repo://github/awesome-copilot/contents/skills/copilot-sdk/SKILL.md", + "Copilot SDK guide", + ), + skillMdEntry("code-review", "skill://code-review/SKILL.md", "Review code"), + skillMdEntry( + "deploy-guide", + "github://acme/platform/skills/deploy-guide/SKILL.md", + "Deployment guide", + ), + ], + }); + + const skills = await listSkillsFromIndex(client); + + expect(skills).toHaveLength(3); + + const copilot = skills!.find((s) => s.name === "copilot-sdk")!; + expect(copilot.uri).toBe("repo://github/awesome-copilot/contents/skills/copilot-sdk/SKILL.md"); + expect(copilot.skillPath).toBe("github/awesome-copilot/contents/skills/copilot-sdk"); + expect(copilot.description).toBe("Copilot SDK guide"); + + const codeReview = skills!.find((s) => s.name === "code-review")!; + expect(codeReview.uri).toBe("skill://code-review/SKILL.md"); + expect(codeReview.skillPath).toBe("code-review"); + + const deploy = skills!.find((s) => s.name === "deploy-guide")!; + expect(deploy.uri).toBe("github://acme/platform/skills/deploy-guide/SKILL.md"); + expect(deploy.skillPath).toBe("acme/platform/skills/deploy-guide"); + }); +}); + +// --------------------------------------------------------------------------- +// Round-trip: generateSkillIndex → listSkillsFromIndex +// --------------------------------------------------------------------------- + +describe("index round-trip (server generates → client consumes)", () => { + it("produces matching SkillSummary entries", async () => { + const map = makeSkillMap([ + makeSkill({ name: "refunds", skillPath: "acme/billing/refunds", description: "Process refunds" }), + makeSkill({ name: "code-review", skillPath: "code-review", description: "Review code" }), + ]); + + const index = generateSkillIndex(map); + const client = mockClientWithIndex(index); + const skills = await listSkillsFromIndex(client); + + expect(skills).toHaveLength(2); + expect(skills!.map((s) => s.name).sort()).toEqual(["code-review", "refunds"]); + expect(skills!.find((s) => s.name === "refunds")!.skillPath).toBe("acme/billing/refunds"); + expect(skills!.find((s) => s.name === "code-review")!.skillPath).toBe("code-review"); + // digest round-trips + expect(skills!.every((s) => SHA256_RE.test(s.digest ?? ""))).toBe(true); + }); + + it("round-trips an archive entry", async () => { + const p = writeArchive("roundtrip.tar.gz"); + const index = generateSkillIndex(new Map(), { + archives: [ + { name: "roundtrip", description: "RT", skillPath: "roundtrip", archivePath: p }, + ], + }); + const client = mockClientWithIndex(index); + const skills = await listSkillsFromIndex(client); + + expect(skills).toHaveLength(1); + expect(skills![0]).toMatchObject({ + name: "roundtrip", + skillPath: "roundtrip", + uri: "skill://roundtrip.tar.gz", + type: "archive", + digest: sha256Digest(fs.readFileSync(p)), + }); + }); +}); + +// --------------------------------------------------------------------------- +// listSkills (resources/list filtering) +// --------------------------------------------------------------------------- + +describe("listSkills", () => { + it("filters to only SKILL.md resources", async () => { + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ + resources: [ + { uri: "skill://code-review/SKILL.md", name: "code-review", description: "Review" }, + { uri: "skill://index.json", name: "skills-index" }, + { uri: "skill://acme/billing/refunds/SKILL.md", name: "refunds", description: "Refunds" }, + ], + }), + readResource: vi.fn(), + }; + + const skills = await listSkills(client); + + expect(skills).toHaveLength(2); + expect(skills[0].name).toBe("code-review"); + expect(skills[1].name).toBe("refunds"); + expect(skills[1].skillPath).toBe("acme/billing/refunds"); + }); + + it("handles pagination", async () => { + const listResources = vi.fn() + .mockResolvedValueOnce({ + resources: [{ uri: "skill://a/SKILL.md", name: "a" }], + nextCursor: "page2", + }) + .mockResolvedValueOnce({ + resources: [{ uri: "skill://b/SKILL.md", name: "b" }], + }); + + const client: SkillsClient = { listResources, readResource: vi.fn() }; + const skills = await listSkills(client); + + expect(skills).toHaveLength(2); + expect(listResources).toHaveBeenCalledTimes(2); + expect(listResources).toHaveBeenCalledWith({ cursor: "page2" }); + }); + + it("returns empty array when no skills exist", async () => { + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ resources: [] }), + readResource: vi.fn(), + }; + + const skills = await listSkills(client); + expect(skills).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// discoverSkills (convenience: index-first with fallback) +// --------------------------------------------------------------------------- + +describe("discoverSkills", () => { + it("returns skills from index when available", async () => { + const client = mockClientWithIndex({ + skills: [skillMdEntry("code-review", "skill://code-review/SKILL.md", "Review code")], + }); + + const skills = await discoverSkills(client); + + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe("code-review"); + expect(client.listResources).not.toHaveBeenCalled(); + }); + + it("falls back to resources/list when index is unavailable", async () => { + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ + resources: [ + { uri: "skill://git-workflow/SKILL.md", name: "git-workflow", description: "Git workflow" }, + ], + }), + readResource: vi.fn().mockRejectedValue(new Error("Not found")), + }; + + const skills = await discoverSkills(client); + + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe("git-workflow"); + }); + + it("falls back to resources/list when index returns empty skills", async () => { + const client: SkillsClient = { + readResource: vi.fn().mockResolvedValue({ + contents: [{ text: JSON.stringify({ skills: [] }) }], + }), + listResources: vi.fn().mockResolvedValue({ + resources: [ + { uri: "skill://fallback/SKILL.md", name: "fallback", description: "Fallback" }, + ], + }), + }; + + const skills = await discoverSkills(client); + + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe("fallback"); + }); + + it("falls back when index has only malformed (unfetchable) entries", async () => { + const client: SkillsClient = { + readResource: vi.fn().mockResolvedValue({ + contents: [{ + text: JSON.stringify({ + skills: [{ frontmatter: { name: "orphan", description: "No url/archives" } }], + }), + }], + }), + listResources: vi.fn().mockResolvedValue({ + resources: [ + { uri: "skill://concrete/SKILL.md", name: "concrete", description: "Concrete" }, + ], + }), + }; + + const skills = await discoverSkills(client); + + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe("concrete"); + }); + + it("returns empty array when nothing found", async () => { + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ resources: [] }), + readResource: vi.fn().mockRejectedValue(new Error("Not found")), + }; + + const skills = await discoverSkills(client); + + expect(skills).toEqual([]); + }); + + it("never returns null", async () => { + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ resources: [] }), + readResource: vi.fn().mockRejectedValue(new Error("Not found")), + }; + + const result = await discoverSkills(client); + expect(result).not.toBeNull(); + expect(Array.isArray(result)).toBe(true); + }); + + it("prefers index over resources/list", async () => { + const client: SkillsClient = { + readResource: vi.fn().mockResolvedValue({ + contents: [{ + text: JSON.stringify({ + skills: [skillMdEntry("from-index", "skill://from-index/SKILL.md", "From index")], + }), + }], + }), + listResources: vi.fn().mockResolvedValue({ + resources: [ + { uri: "skill://from-list/SKILL.md", name: "from-list", description: "From list" }, + ], + }), + }; + + const skills = await discoverSkills(client); + + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe("from-index"); + expect(client.listResources).not.toHaveBeenCalled(); + }); +}); + +// --------------------------------------------------------------------------- +// discoverAndBuildCatalog (convenience: discover + catalog in one call) +// --------------------------------------------------------------------------- + +describe("discoverAndBuildCatalog", () => { + it("returns skills and catalog text", async () => { + const client = mockClientWithIndex({ + skills: [skillMdEntry("code-review", "skill://code-review/SKILL.md", "Review code")], + }); + + const result = await discoverAndBuildCatalog(client, { serverName: "my-server" }); + + expect(result.skills).toHaveLength(1); + expect(result.skills[0].name).toBe("code-review"); + expect(result.catalog).toContain(""); + expect(result.catalog).toContain("`my-server`"); + expect(result.catalog).toContain("`read_resource`"); + }); + + it("uses default toolName from READ_RESOURCE_TOOL", async () => { + const client = mockClientWithIndex({ + skills: [skillMdEntry("a", "skill://a/SKILL.md", "A")], + }); + + const result = await discoverAndBuildCatalog(client, { serverName: "test-server" }); + expect(result.catalog).toContain("`read_resource`"); + }); + + it("allows overriding toolName", async () => { + const client = mockClientWithIndex({ + skills: [skillMdEntry("a", "skill://a/SKILL.md", "A")], + }); + + const result = await discoverAndBuildCatalog(client, { + serverName: "test-server", + toolName: "ReadMcpResourceTool", + }); + + expect(result.catalog).toContain("`ReadMcpResourceTool`"); + expect(result.catalog).not.toContain("`read_resource`"); + }); + + it("returns empty catalog when no skills found", async () => { + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ resources: [] }), + readResource: vi.fn().mockRejectedValue(new Error("Not found")), + }; + + const result = await discoverAndBuildCatalog(client, { serverName: "empty-server" }); + + expect(result.skills).toEqual([]); + expect(result.catalog).toBe(""); + }); + + it("works without options entirely (serverName is optional)", async () => { + const client = mockClientWithIndex({ + skills: [skillMdEntry("a", "skill://a/SKILL.md", "A")], + }); + + const result = await discoverAndBuildCatalog(client); + + expect(result.skills).toHaveLength(1); + expect(result.catalog).not.toContain("with server"); + expect(result.catalog).toContain("with the skill's URI"); + expect(result.catalog).not.toContain(""); + }); + + it("threads serverInEntries through to the catalog", async () => { + const client = mockClientWithIndex({ + skills: [skillMdEntry("a", "skill://a/SKILL.md", "A")], + }); + + const result = await discoverAndBuildCatalog(client, { + serverName: "my-server", + serverInEntries: true, + }); + + expect(result.catalog).toContain("my-server"); + }); +}); diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts new file mode 100644 index 0000000..0b7effb --- /dev/null +++ b/typescript/sdk/src/index.ts @@ -0,0 +1,69 @@ +/** + * Skills Extension SDK — Main barrel exports. + * + * Exports shared types, URI utilities, and MIME utilities. + * Server-specific and client-specific exports are available via + * subpath imports: "@modelcontextprotocol/experimental-ext-skills/server" + * and "@modelcontextprotocol/experimental-ext-skills/client". + */ + +export type { + SkillDocument, + SkillMetadata, + SkillSummary, + SkillArchiveRef, + SkillIndexEntry, + SkillIndex, + SkillArchiveDeclaration, + ArchiveFormat, + UnpackedSkillArchive, + ExtractArchiveOptions, + SkillsCatalogOptions, + DiscoverSkillsOptions, + DiscoverCatalogOptions, + DiscoverCatalogResult, + InstructionsUriExtractor, + RegisterSkillResourcesOptions, +} from "./types.js"; + +export { + detectArchiveFormat, + stripArchiveSuffix, + archiveMimeType, + archiveSuffix, + extractSkillArchive, +} from "./archive.js"; + +export { + DIRECTORY_READ_METHOD, + INODE_DIRECTORY_MIME, + DEFAULT_DIRECTORY_PAGE_SIZE, + DirectoryReadRequestSchema, + DirectoryReadResultSchema, + buildDirectoryTree, + makeDirectoryReadHandler, +} from "./directory.js"; +export type { + DirectoryChild, + DirectoryReadResult, + DirectoryReadHandlerOptions, +} from "./directory.js"; + +export { SKILLS_EXTENSION_ID } from "./resource-extensions.js"; +export type { SkillsExtensionCapability } from "./resource-extensions.js"; + +export { + SKILL_URI_SCHEME, + SKILL_FILENAME, + INDEX_JSON_URI, + parseSkillUri, + resolveSkillFileUri, + buildSkillUri, + isSkillContentUri, + isIndexJsonUri, + isValidSkillName, + extractSkillPathFromUri, +} from "./uri.js"; +export type { ParsedSkillUri } from "./uri.js"; + +export { getMimeType, isTextMimeType } from "./mime.js"; diff --git a/typescript/sdk/src/instructions.test.ts b/typescript/sdk/src/instructions.test.ts new file mode 100644 index 0000000..7c55466 --- /dev/null +++ b/typescript/sdk/src/instructions.test.ts @@ -0,0 +1,310 @@ +/** + * Tests for the third SEP discovery path: mining server `instructions` + * for skill URIs. + */ + +import { describe, it, expect, vi } from "vitest"; +import { + extractSkillUrisFromInstructions, + listSkillsFromInstructions, + discoverSkills, +} from "./_client.js"; +import type { SkillsClient } from "./_client.js"; + +// --------------------------------------------------------------------------- +// extractSkillUrisFromInstructions +// --------------------------------------------------------------------------- + +describe("extractSkillUrisFromInstructions", () => { + it("returns empty array for missing or empty instructions", () => { + expect(extractSkillUrisFromInstructions(undefined)).toEqual([]); + expect(extractSkillUrisFromInstructions("")).toEqual([]); + }); + + it("extracts a single skill:// URI from prose", () => { + const text = "Read skill://git-workflow/SKILL.md before opening a PR."; + expect(extractSkillUrisFromInstructions(text)).toEqual([ + "skill://git-workflow/SKILL.md", + ]); + }); + + it("extracts multiple URIs and deduplicates them", () => { + const text = ` + Use skill://acme/billing/refunds/SKILL.md for refunds. + For onboarding, see skill://acme/onboarding/SKILL.md. + Refunds again: skill://acme/billing/refunds/SKILL.md. + `; + expect(extractSkillUrisFromInstructions(text)).toEqual([ + "skill://acme/billing/refunds/SKILL.md", + "skill://acme/onboarding/SKILL.md", + ]); + }); + + it("handles non-skill schemes per the SEP (any scheme + SKILL.md)", () => { + const text = + "We expose github://acme/platform/skills/deploy/SKILL.md and repo://x/y/SKILL.md."; + expect(extractSkillUrisFromInstructions(text)).toEqual([ + "github://acme/platform/skills/deploy/SKILL.md", + "repo://x/y/SKILL.md", + ]); + }); + + it("strips trailing punctuation from prose URIs", () => { + const text = "See (skill://x/SKILL.md). Or skill://y/SKILL.md, then continue."; + const uris = extractSkillUrisFromInstructions(text); + expect(uris).toContain("skill://x/SKILL.md"); + expect(uris).toContain("skill://y/SKILL.md"); + // None of these should pick up a trailing `,` or `.` + for (const uri of uris) { + expect(uri.endsWith(".md")).toBe(true); + } + }); + + it("ignores non-SKILL.md URLs entirely", () => { + const text = "Documentation at https://example.com/docs and read foo://bar/baz.txt."; + expect(extractSkillUrisFromInstructions(text)).toEqual([]); + }); + + it("matches case-insensitively on SKILL.md", () => { + const text = "Lower: skill://x/skill.md. Mixed: skill://y/Skill.MD."; + expect(extractSkillUrisFromInstructions(text)).toEqual([ + "skill://x/skill.md", + "skill://y/Skill.MD", + ]); + }); +}); + +// --------------------------------------------------------------------------- +// listSkillsFromInstructions +// --------------------------------------------------------------------------- + +describe("listSkillsFromInstructions", () => { + it("reads each URI and parses frontmatter", async () => { + const readResource = vi.fn(async ({ uri }: { uri: string }) => { + const content = + uri === "skill://x/SKILL.md" + ? "---\nname: x\ndescription: First skill\n---\n# X" + : "---\nname: y\ndescription: Second skill\n---\n# Y"; + return { contents: [{ text: content }] }; + }); + const client: SkillsClient = { listResources: vi.fn(), readResource }; + + const summaries = await listSkillsFromInstructions( + client, + "Use skill://x/SKILL.md and skill://y/SKILL.md.", + ); + + expect(summaries).toHaveLength(2); + expect(summaries[0]).toMatchObject({ + name: "x", + skillPath: "x", + uri: "skill://x/SKILL.md", + description: "First skill", + }); + expect(summaries[1].name).toBe("y"); + }); + + it("silently skips URIs that fail to read", async () => { + const readResource = vi.fn(async ({ uri }: { uri: string }) => { + if (uri === "skill://broken/SKILL.md") + throw new Error("server: not found"); + return { + contents: [{ text: "---\nname: ok\ndescription: ok\n---\n# OK" }], + }; + }); + const client: SkillsClient = { listResources: vi.fn(), readResource }; + + const summaries = await listSkillsFromInstructions( + client, + "Try skill://broken/SKILL.md and skill://ok/SKILL.md.", + ); + + expect(summaries).toHaveLength(1); + expect(summaries[0].uri).toBe("skill://ok/SKILL.md"); + }); + + it("returns empty when instructions name no URIs", async () => { + const client: SkillsClient = { + listResources: vi.fn(), + readResource: vi.fn(), + }; + expect(await listSkillsFromInstructions(client, "no URIs here")).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// discoverSkills() integration with instructions +// --------------------------------------------------------------------------- + +describe("discoverSkills with server instructions", () => { + it("does NOT mine instructions by default", async () => { + const indexJson = { + skills: [ + { + frontmatter: { name: "from-index", description: "Index" }, + url: "skill://from-index/SKILL.md", + digest: "sha256:" + "a".repeat(64), + }, + ], + }; + const readResource = vi.fn(async ({ uri }: { uri: string }) => { + if (uri === "skill://index.json") + return { contents: [{ text: JSON.stringify(indexJson) }] }; + throw new Error("server should not be asked for this URI"); + }); + const getInstructions = vi.fn( + () => "Read skill://from-instructions/SKILL.md when needed.", + ); + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ resources: [] }), + readResource, + getInstructions, + }; + + const skills = await discoverSkills(client); + + expect(skills.map((s) => s.name)).toEqual(["from-index"]); + expect(getInstructions).not.toHaveBeenCalled(); + }); + + it("merges instructions URIs with index entries when opted in", async () => { + const indexJson = { + skills: [ + { + frontmatter: { name: "from-index", description: "Index" }, + url: "skill://from-index/SKILL.md", + digest: "sha256:" + "a".repeat(64), + }, + ], + }; + const readResource = vi.fn(async ({ uri }: { uri: string }) => { + if (uri === "skill://index.json") + return { contents: [{ text: JSON.stringify(indexJson) }] }; + if (uri === "skill://from-instructions/SKILL.md") + return { + contents: [ + { + text: "---\nname: from-instructions\ndescription: From instructions\n---\n# X", + }, + ], + }; + throw new Error("not found"); + }); + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ resources: [] }), + readResource, + getInstructions: () => + "Read skill://from-instructions/SKILL.md when needed.", + }; + + const skills = await discoverSkills(client, { instructions: true }); + + expect(skills.map((s) => s.name).sort()).toEqual([ + "from-index", + "from-instructions", + ]); + }); + + it("does not duplicate an instructions URI that's already in the index", async () => { + const indexJson = { + skills: [ + { + frontmatter: { name: "shared", description: "Shared" }, + url: "skill://shared/SKILL.md", + digest: "sha256:" + "a".repeat(64), + }, + ], + }; + const readResource = vi.fn(async ({ uri }: { uri: string }) => { + if (uri === "skill://index.json") + return { contents: [{ text: JSON.stringify(indexJson) }] }; + return { + contents: [{ text: "---\nname: shared\ndescription: S\n---\n# S" }], + }; + }); + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ resources: [] }), + readResource, + getInstructions: () => "See skill://shared/SKILL.md.", + }; + + const skills = await discoverSkills(client, { instructions: true }); + expect(skills).toHaveLength(1); + expect(skills[0].uri).toBe("skill://shared/SKILL.md"); + }); + + it("uses instructions when index is unavailable, before resources/list", async () => { + const readResource = vi.fn(async ({ uri }: { uri: string }) => { + if (uri === "skill://index.json") throw new Error("no index"); + return { + contents: [ + { text: "---\nname: from-instr\ndescription: I\n---\n# X" }, + ], + }; + }); + const listResources = vi.fn(); + const client: SkillsClient = { + listResources, + readResource, + getInstructions: () => "Use skill://from-instr/SKILL.md.", + }; + + const skills = await discoverSkills(client, { instructions: true }); + expect(skills).toHaveLength(1); + expect(skills[0].uri).toBe("skill://from-instr/SKILL.md"); + expect(listResources).not.toHaveBeenCalled(); + }); + + it("falls through to resources/list when index empty and instructions opted out", async () => { + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ + resources: [ + { + uri: "skill://from-list/SKILL.md", + name: "from-list", + description: "L", + }, + ], + }), + readResource: vi.fn().mockRejectedValue(new Error("no index")), + getInstructions: () => "Some instructions with skill://x/SKILL.md.", + }; + + const skills = await discoverSkills(client); + expect(skills).toHaveLength(1); + expect(skills[0].name).toBe("from-list"); + }); + + it("uses a custom extractor when provided", async () => { + const readResource = vi.fn(async ({ uri }: { uri: string }) => { + if (uri === "skill://index.json") throw new Error("no index"); + return { + contents: [{ text: "---\nname: custom\ndescription: C\n---\n# C" }], + }; + }); + const client: SkillsClient = { + listResources: vi.fn().mockResolvedValue({ resources: [] }), + readResource, + getInstructions: () => + // Instructions list URIs in a non-standard JSON-array form that the + // built-in regex would still match, but we want to demonstrate the + // custom extractor takes precedence and can return whatever it wants. + '{"my-skills":["skill://custom/SKILL.md"]}', + }; + + const customExtractor = vi.fn( + (text: string) => + // Pretend we parse the JSON and return the array + JSON.parse(text)["my-skills"] as string[], + ); + + const skills = await discoverSkills(client, { + instructions: true, + extractor: customExtractor, + }); + + expect(customExtractor).toHaveBeenCalledOnce(); + expect(skills).toHaveLength(1); + expect(skills[0].uri).toBe("skill://custom/SKILL.md"); + }); +}); diff --git a/typescript/sdk/src/mime.ts b/typescript/sdk/src/mime.ts new file mode 100644 index 0000000..83ab807 --- /dev/null +++ b/typescript/sdk/src/mime.ts @@ -0,0 +1,52 @@ +/** + * MIME type utilities for skill documents. + */ + +import * as path from "node:path"; + +/** 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"; +} + +/** + * Check if a MIME type represents text content (as opposed to binary). + * Matches skillsdotnet's logic: text/* types, plus application/json, + * application/xml, application/javascript, and +json/+xml suffixes. + */ +export function isTextMimeType(mimeType: string): boolean { + if (mimeType.startsWith("text/")) return true; + if (mimeType === "application/json") return true; + if (mimeType === "application/xml") return true; + if (mimeType === "application/javascript") return true; + if (mimeType.endsWith("+json")) return true; + if (mimeType.endsWith("+xml")) return true; + return false; +} diff --git a/typescript/sdk/src/register.test.ts b/typescript/sdk/src/register.test.ts new file mode 100644 index 0000000..4901595 --- /dev/null +++ b/typescript/sdk/src/register.test.ts @@ -0,0 +1,213 @@ +/** + * Tests for registerSkillResources() — resource registration, `_meta` + * threading, the optional `skill://index.json`, and the SEP-2640 + * `resources/directory/read` handler. + */ + +import { describe, it, expect } from "vitest"; +import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { registerSkillResources } from "./_server.js"; +import { DIRECTORY_READ_METHOD } from "./directory.js"; +import type { SkillMetadata } from "./types.js"; + +// --------------------------------------------------------------------------- +// Stub MCP server that records resource() and setRequestHandler() calls. +// --------------------------------------------------------------------------- + +interface RegisteredCall { + name: string; + uriOrTemplate: string | ResourceTemplate; + metadata: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (...args: any[]) => any; +} + +interface HandlerCall { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + handler: (...args: any[]) => any; +} + +function makeStubServer() { + const calls: RegisteredCall[] = []; + const handlers: HandlerCall[] = []; + return { + calls, + handlers, + resource(...args: unknown[]) { + const [name, uriOrTemplate, metadata, callback] = args as [ + string, + string | ResourceTemplate, + Record, + (...a: unknown[]) => unknown, + ]; + calls.push({ name, uriOrTemplate, metadata, callback }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setRequestHandler(schema: any, handler: (...a: any[]) => any) { + handlers.push({ schema, handler }); + }, + }; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function emptySkillMap(): Map { + return new Map(); +} + +function skill(overrides: Partial & { + name: string; + skillPath: string; +}): SkillMetadata { + return { + description: "desc", + absolutePath: `/skills/${overrides.skillPath}/SKILL.md`, + skillDir: `/skills/${overrides.skillPath}`, + documents: [], + size: 100, + lastModified: "2026-01-01T00:00:00.000Z", + frontmatter: { name: overrides.name, description: "desc" }, + digest: "sha256:" + "0".repeat(64), + ...overrides, + }; +} + +// --------------------------------------------------------------------------- +// _meta threading +// --------------------------------------------------------------------------- + +describe("registerSkillResources — _meta threading", () => { + it("threads SkillMetadata.meta into the SKILL.md resource _meta", () => { + const server = makeStubServer(); + const skillMap = new Map([ + [ + "code-review", + skill({ + name: "code-review", + skillPath: "code-review", + description: "Review code", + meta: { "io.modelcontextprotocol.skills/provenance": "acme/internal" }, + }), + ], + ]); + + registerSkillResources(server, skillMap, "/skills", { template: false }); + + const skillCall = server.calls.find((c) => c.name === "code-review"); + expect(skillCall).toBeDefined(); + expect(skillCall!.metadata._meta).toEqual({ + "io.modelcontextprotocol.skills/provenance": "acme/internal", + }); + }); + + it("omits _meta from registration when SkillMetadata.meta is unset", () => { + const server = makeStubServer(); + const skillMap = new Map([ + ["code-review", skill({ name: "code-review", skillPath: "code-review", description: "Review code" })], + ]); + + registerSkillResources(server, skillMap, "/skills", { template: false }); + const skillCall = server.calls.find((c) => c.name === "code-review"); + expect(skillCall!.metadata._meta).toBeUndefined(); + }); +}); + +// --------------------------------------------------------------------------- +// skill://index.json registration +// --------------------------------------------------------------------------- + +describe("registerSkillResources — index resource", () => { + it("registers skill://index.json by default", () => { + const server = makeStubServer(); + registerSkillResources(server, emptySkillMap(), "/skills", { template: false }); + expect(server.calls.find((c) => c.name === "skills-index")).toBeDefined(); + }); + + it("omits skill://index.json when index: false", () => { + const server = makeStubServer(); + registerSkillResources(server, emptySkillMap(), "/skills", { + template: false, + index: false, + }); + expect(server.calls.find((c) => c.name === "skills-index")).toBeUndefined(); + }); + + it("registers the catch-all skill-file template when template: true", () => { + const server = makeStubServer(); + registerSkillResources(server, emptySkillMap(), "/skills", { template: true }); + const catchAll = server.calls.find((c) => c.name === "skill-file"); + expect(catchAll).toBeDefined(); + expect(catchAll!.uriOrTemplate).toBeInstanceOf(ResourceTemplate); + }); +}); + +// --------------------------------------------------------------------------- +// resources/directory/read handler +// --------------------------------------------------------------------------- + +describe("registerSkillResources — directoryRead", () => { + it("does not register a directory/read handler by default", () => { + const server = makeStubServer(); + registerSkillResources(server, emptySkillMap(), "/skills", { template: false }); + expect(server.handlers).toHaveLength(0); + }); + + it("registers a resources/directory/read handler when directoryRead: true", () => { + const server = makeStubServer(); + registerSkillResources(server, emptySkillMap(), "/skills", { + template: false, + directoryRead: true, + }); + + expect(server.handlers).toHaveLength(1); + // The schema routes by its `method` literal. + const method = server.handlers[0].schema.shape.method.value; + expect(method).toBe(DIRECTORY_READ_METHOD); + }); + + it("registers the handler on the low-level server (server.server) when present", () => { + const low = makeStubServer(); + const high = { resource: low.resource, server: low }; + registerSkillResources(high, emptySkillMap(), "/skills", { + template: false, + directoryRead: true, + }); + expect(low.handlers).toHaveLength(1); + }); + + it("serves a directory listing for a registered skill", async () => { + const server = makeStubServer(); + const skillMap = new Map([ + [ + "code-review", + skill({ + name: "code-review", + skillPath: "code-review", + documents: [ + { path: "references/GUIDE.md", mimeType: "text/markdown", size: 10 }, + ], + }), + ], + ]); + + registerSkillResources(server, skillMap, "/skills", { + template: false, + directoryRead: true, + }); + + const handler = server.handlers[0].handler; + const result = await handler({ + method: DIRECTORY_READ_METHOD, + params: { uri: "skill://code-review" }, + }); + + const names = result.resources.map((r: { name: string }) => r.name).sort(); + expect(names).toEqual(["SKILL.md", "references"]); + const refDir = result.resources.find((r: { name: string }) => r.name === "references"); + expect(refDir.mimeType).toBe("inode/directory"); + }); +}); diff --git a/typescript/sdk/src/resource-extensions.ts b/typescript/sdk/src/resource-extensions.ts new file mode 100644 index 0000000..12ce858 --- /dev/null +++ b/typescript/sdk/src/resource-extensions.ts @@ -0,0 +1,61 @@ +/** + * Extension declaration for the Skills Extension SEP. + * + * Declares the extension capability: + * capabilities.extensions["io.modelcontextprotocol/skills"] = { ... } + * + * Uses the SDK's native registerCapabilities API (v1.29.0+). + */ + +/** Reverse-domain identifier for the skills extension (SEP-2640). */ +export const SKILLS_EXTENSION_ID = "io.modelcontextprotocol/skills"; + +/** + * The skills extension capability object a server advertises in its + * `initialize` response. An empty object means "supports the extension with + * no optional features". + */ +export interface SkillsExtensionCapability { + /** + * Server implements the SEP-2640 `resources/directory/read` method. + * Default `false`. Clients MUST NOT call `resources/directory/read` + * against a server that has not declared `directoryRead: true`. + */ + directoryRead?: boolean; +} + +/** + * Minimal structural interface for a Server that supports registerCapabilities. + * Using a structural type avoids issues with duplicate SDK installations + * causing private-property type incompatibilities (same pattern as SkillsClient). + */ +export interface SkillsServer { + registerCapabilities(capabilities: { + extensions?: Record; + }): void; +} + +/** @deprecated Use {@link SkillsServer} instead. */ +export type ServerInternals = SkillsServer; + +/** + * Declare the skills extension in server capabilities (SEP-2640). + * + * Registers: + * capabilities.extensions["io.modelcontextprotocol/skills"] = capability + * + * Pass `{ directoryRead: true }` when the server implements + * `resources/directory/read` (see `registerSkillResources({ directoryRead: + * true })`). With no argument an empty capability object is declared. + * + * Must be called BEFORE server.connect() — capabilities are sent during the + * initialize handshake. + */ +export function declareSkillsExtension( + server: SkillsServer, + capability: SkillsExtensionCapability = {}, +): void { + server.registerCapabilities({ + extensions: { [SKILLS_EXTENSION_ID]: capability }, + }); +} diff --git a/typescript/sdk/src/server/index.ts b/typescript/sdk/src/server/index.ts new file mode 100644 index 0000000..5b4cf23 --- /dev/null +++ b/typescript/sdk/src/server/index.ts @@ -0,0 +1,38 @@ +/** + * Server-side exports for the Skills Extension SDK. + */ + +export { + discoverSkills, + registerSkillResources, + generateSkillIndex, + loadSkillContent, + loadDocument, + scanDocuments, + isPathWithinBase, + sha256Digest, +} from "../_server.js"; +export type { GenerateSkillIndexOptions } from "../_server.js"; + +export { + declareSkillsExtension, + SKILLS_EXTENSION_ID, +} from "../resource-extensions.js"; +export type { + SkillsServer, + ServerInternals, + SkillsExtensionCapability, +} from "../resource-extensions.js"; + +export { + DIRECTORY_READ_METHOD, + INODE_DIRECTORY_MIME, + DirectoryReadRequestSchema, + buildDirectoryTree, + makeDirectoryReadHandler, +} from "../directory.js"; +export type { + DirectoryChild, + DirectoryReadResult, + DirectoryReadHandlerOptions, +} from "../directory.js"; diff --git a/typescript/sdk/src/types.ts b/typescript/sdk/src/types.ts new file mode 100644 index 0000000..3a638cd --- /dev/null +++ b/typescript/sdk/src/types.ts @@ -0,0 +1,395 @@ +/** + * Type definitions for the Skills Extension SDK. + * + * Key design point: SkillMetadata separates `skillPath` (the multi-segment + * URI locator, e.g., "acme/billing/refunds") from `name` (the skill identity + * from YAML frontmatter). The URI path is a locator, not an identifier; the + * skill map is keyed by `skillPath` since two skills could share a frontmatter + * name across different directories. + */ + +/** + * 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. + * + * - `name` is the skill's identity from frontmatter + * - `skillPath` is the multi-segment URI locator (e.g., "acme/billing/refunds") + * These are intentionally decoupled. + */ +export interface SkillMetadata { + /** Skill identity from YAML frontmatter — NOT derived from path */ + name: string; + /** Multi-segment URI locator (e.g., "acme/billing/refunds") */ + skillPath: string; + /** Skill description from YAML frontmatter */ + description: string; + /** + * The skill's full SKILL.md YAML frontmatter, parsed to a plain object. + * Per SEP-2640 this block is copied verbatim into the skill's + * `skill://index.json` entry (`frontmatter`), so `name`/`description` + * are always present and any other authored fields (`license`, + * `metadata`, compatibility, …) pass through unchanged. + */ + frontmatter: Record; + /** + * SHA-256 digest of the SKILL.md file's raw bytes, formatted as + * `sha256:{hex}` (64 lowercase hex). Emitted as the entry `digest` in + * `skill://index.json` alongside `url`, per SEP-2640. + */ + digest: string; + /** Absolute filesystem path to the SKILL.md file */ + absolutePath: string; + /** Absolute filesystem path to the skill's directory */ + skillDir: string; + /** + * Custom MCP resource `_meta` for this skill's `SKILL.md` resource. + * + * Per `docs/skill-meta-keys.md`, most skills do NOT need `_meta` — name, + * description, version, allowed-tools, and other skill-level semantics + * belong in frontmatter (the resource body), not duplicated here. Use + * `_meta` only for transport-layer concerns that have no frontmatter + * equivalent (provenance the host needs without reading content, + * content-integrity hashes, etc.) and prefix custom keys with the + * `io.modelcontextprotocol.skills/` reverse-domain namespace. + * + * The SDK never auto-projects frontmatter into `_meta`; it's set only + * when the caller provides this field. + */ + meta?: Record; + /** Audience annotation for this skill's resources (e.g., ["assistant"] or ["user", "assistant"]) */ + audience?: string[]; + /** Supplementary files found in the skill directory */ + documents: SkillDocument[]; + /** SKILL.md file size in bytes */ + size: number; + /** ISO 8601 timestamp from SKILL.md file mtime */ + lastModified: string; +} + +/** + * Lightweight client-side summary of a discovered skill. + * Built from resources/list results and URI parsing. + */ +export interface SkillSummary { + /** Skill name (from resource description or frontmatter) */ + name: string; + /** Multi-segment skill path parsed from URI */ + skillPath: string; + /** + * URI to read this skill from. + * + * For `type: "skill-md"`: the SKILL.md resource URI — read directly via + * `resources/read` to get the markdown content. + * + * For `type: "archive"`: the archive resource URI (e.g. + * `skill://pdf-processing.tar.gz`) — fetch and unpack via + * `readSkillArchive()`. The post-unpack SKILL.md lives at + * `skill:///SKILL.md`. + */ + uri: string; + /** + * Distribution type, derived from the index entry shape (a `url` ⇒ + * `"skill-md"`, archives-only ⇒ `"archive"`). When omitted (e.g. skills + * discovered via `resources/list` without an index), assume `"skill-md"`. + */ + type?: "skill-md" | "archive"; + /** Skill description (from frontmatter / resource metadata) */ + description?: string; + /** MIME type of the resource */ + mimeType?: string; + /** + * SHA-256 digest of the resource named by `uri`, formatted `sha256:{hex}`, + * when the index entry carried one. For `type: "skill-md"` this is the + * SKILL.md digest; for `type: "archive"` it is the chosen archive's + * digest. Pass to {@link verifyDigest} to honor the SEP's integrity MUST. + */ + digest?: string; + /** + * All archive representations advertised for this skill in the index + * (each with its own `url`, `mimeType`, and `digest`), when present. + */ + archives?: SkillArchiveRef[]; +} + +/** + * One archive representation of a skill within a `skill://index.json` entry. + * + * Per SEP-2640, a skill MAY advertise one or more archive forms of its + * directory. Each archive is a single resource (mime type e.g. + * `application/gzip` or `application/zip`) whose contents unpack into the + * skill's URI namespace (`SKILL.md` at the archive root). Each carries its + * own SHA-256 `digest` for caching/integrity. + */ +export interface SkillArchiveRef { + /** Resource URI of the archive (e.g. `skill://pdf-processing.tar.gz`). */ + url: string; + /** Archive media type (e.g. `application/gzip`, `application/zip`). */ + mimeType: string; + /** SHA-256 digest of the archive bytes, formatted `sha256:{hex}`. */ + digest: string; +} + +/** + * An entry in the `skill://index.json` MCP discovery index (SEP-2640). + * + * Entries are **type-less**: a skill is described by its verbatim + * `frontmatter` plus how it can be retrieved. Every entry MUST include a + * `url` (with `digest`), a non-empty `archives` array, or both. `name` and + * `description` are NOT top-level fields — they live inside `frontmatter` + * (the Agent Skills spec requires both, so they are always present). + */ +export interface SkillIndexEntry { + /** + * Verbatim copy of the skill's `SKILL.md` YAML frontmatter, rendered as a + * JSON object. Always carries `name` and `description`; any other authored + * fields pass through unchanged. + */ + frontmatter: Record; + /** + * Resource URI of the skill's `SKILL.md`, when served as an individual + * file. REQUIRED when `digest` is present; absent for archive-only skills. + */ + url?: string; + /** + * SHA-256 digest of the `SKILL.md` file, formatted `sha256:{hex}`. + * REQUIRED whenever `url` is present. + */ + digest?: string; + /** Archive distributions of the skill. Non-empty when present. */ + archives?: SkillArchiveRef[]; +} + +/** + * Archive format. Per SEP-2640, hosts MUST support both. Format determines + * the served `mimeType` (`application/gzip` or `application/zip`) and + * the URL suffix (`.tar.gz` or `.zip`). + */ +export type ArchiveFormat = "tar.gz" | "zip"; + +/** + * Server-side declaration for an archive-distributed skill. + * Passed to registerSkillResources() to register the archive as an MCP + * resource and include it in skill://index.json. + * + * The archive is served as a single resource at + * `skill://.`. After the host unpacks it, files are + * addressable at `skill:///` — identical namespace + * to individual-file distribution. + */ +export interface SkillArchiveDeclaration { + /** + * Skill name from frontmatter; MUST equal the final segment of `skillPath` + * per SEP-2640. + */ + name: string; + /** Skill description from frontmatter */ + description: string; + /** + * Full SKILL.md frontmatter for this archived skill, copied verbatim into + * the skill's `skill://index.json` entry (`frontmatter`). The archive is + * not unpacked at registration, so the SDK cannot read it from inside the + * archive — provide it here to preserve authored fields (`license`, + * `metadata`, …). When omitted, the index entry falls back to + * `{ name, description }`. + */ + frontmatter?: Record; + /** + * Multi-segment skill path that the archive unpacks to. The final segment + * MUST equal `name`. + */ + skillPath: string; + /** + * Local filesystem path to the prebuilt archive. The SDK reads this once + * at registration and serves the bytes on `resources/read`. + */ + archivePath: string; + /** + * Archive format. Defaults to inference from `archivePath` suffix + * (`.tar.gz`/`.tgz` → `tar.gz`, `.zip` → `zip`). + */ + format?: ArchiveFormat; +} + +/** + * Result of unpacking a skill archive. + * Maps file paths (relative to skill root, forward-slash separated) to + * raw byte contents. + */ +export interface UnpackedSkillArchive { + /** Files in the archive, keyed by relative path. */ + files: Map; + /** Total uncompressed bytes across all entries. */ + totalSize: number; +} + +/** Options for archive extraction. */ +export interface ExtractArchiveOptions { + /** Maximum total uncompressed bytes. Default: 50MB. */ + maxTotalSize?: number; + /** Maximum bytes per single file. Default: 10MB. */ + maxFileSize?: number; + /** Maximum number of entries. Default: 1024. */ + maxEntries?: number; +} + +/** + * The `skill://index.json` resource content (SEP-2640). + * + * The WG owns this schema; it is intentionally decoupled from the + * agentskills.io `.well-known` discovery format. The index carries no + * `$schema` / version marker — the format is versioned by the extension + * itself. + */ +export interface SkillIndex { + /** Array of skill entries */ + skills: SkillIndexEntry[]; +} + +/** + * Options for buildSkillsCatalog(). + */ +export interface SkillsCatalogOptions { + /** Tool name the model should call to read skill content */ + toolName: string; + /** + * MCP server name the model should target. Omit when the configured + * `toolName` does not accept a `server` parameter (e.g., a host-scoped + * reader that only takes `uri`) — the behavioral instructions will drop + * the server clause so the prompt doesn't mention an unused argument. + */ + serverName?: string; + /** + * Inject `{name}` into each `` entry alongside + * the URI. Default: false. The host SKILL.md flags per-entry server-name + * placement as a way to keep first-call activation reliability ~90% for + * `(server, uri)` reader tools (vs ~33% with the server name only in the + * wrapper prose). It's not in SEP-2640, so the SDK leaves it off by + * default and lets hosts opt in. Has no effect unless `serverName` is + * also set. + */ + serverInEntries?: boolean; +} + +/** + * Custom extractor for skill URIs in a server's `instructions` string. + * Receives the raw instructions text and returns a deduplicated array + * of URI strings. Replaces the SDK's built-in regex extractor entirely + * — useful when the server uses a non-standard URI convention in prose + * (e.g., URIs inside code fences, multi-line URIs, domain-specific + * schemes that look like prose tokens). + */ +export type InstructionsUriExtractor = (instructions: string) => string[]; + +/** + * Options for discoverSkills(). All fields are optional; defaults match + * the SEP's recommended index-first / list-fallback strategy without + * mining server instructions. + */ +export interface DiscoverSkillsOptions { + /** + * Mine the server's `instructions` string for skill URIs and merge them + * with index entries (deduplicated by URI). Off by default — most + * servers don't name skill URIs in their instructions, and enabling + * this costs one `resources/read` round-trip per URI mentioned. Turn + * on for documentation-server / gateway / template-only servers per + * the SEP's third discovery path. + * + * @default false + */ + instructions?: boolean; + /** + * Custom extractor used when `instructions: true`. When omitted, the + * SDK's built-in regex extractor (`extractSkillUrisFromInstructions`) + * is used. + */ + extractor?: InstructionsUriExtractor; +} + +/** + * Options for discoverAndBuildCatalog(). + */ +export interface DiscoverCatalogOptions { + /** + * MCP server name the model should target. Optional. Set when the + * configured `toolName` accepts a `server` parameter (e.g., the bundled + * `READ_RESOURCE_TOOL`); omit for host-scoped readers that take only + * `uri`. The host SKILL.md observes activation reliability ~90% (vs ~33%) + * when the server name appears in the prompt — but that's empirical + * guidance, not SEP, so the SDK no longer forces it. + */ + serverName?: string; + /** Tool name the model should call to read resources. Default: "read_resource" */ + toolName?: string; + /** + * Mine the server's `instructions` for skill URIs (passed through to + * `discoverSkills()`). Default: false. + */ + instructions?: boolean; + /** Custom URI extractor for `instructions`. Default: built-in regex. */ + extractor?: InstructionsUriExtractor; + /** + * Inject `{name}` into each `` entry. Default: + * false. Has no effect unless `serverName` is set. + */ + serverInEntries?: boolean; +} + +/** + * Result of discoverAndBuildCatalog(). + */ +export interface DiscoverCatalogResult { + /** Discovered skills */ + skills: SkillSummary[]; + /** System prompt catalog text (empty string if no skills found) */ + catalog: string; +} + +/** + * Options for registerSkillResources(). + */ +export interface RegisterSkillResourcesOptions { + /** Register the resource template for supporting files. Default: true */ + template?: boolean; + /** + * Register the well-known `skill://index.json` discovery resource. Default: + * true. Set to `false` for servers whose skill catalog is large, generated + * on demand, or otherwise unenumerable — per SEP-2640 a server MAY decline + * to expose the index. Skills remain individually readable via + * `resources/read` regardless. + */ + index?: boolean; + /** Audience annotation for skill resources. Default: ["assistant"] */ + audience?: string[]; + /** + * Archive-distributed skills to register and include in `skill://index.json`. + * Each declaration's archive file is read from disk and served as a single + * resource at `skill://.`. + */ + archives?: SkillArchiveDeclaration[]; + /** + * Implement the SEP-2640 `resources/directory/read` method so hosts can + * enumerate the files under each individually-served skill directory + * (an `ls`-style, metadata-only, paginated listing). Default `false`. + * + * When `true`, the SDK registers a handler on the server's low-level + * request router. The server MUST also advertise the capability by calling + * `declareSkillsExtension(server, { directoryRead: true })` before + * `connect()` — capabilities are sent during the initialize handshake and + * cannot be added by `registerSkillResources` after the fact. Note that + * archive-distributed skills are opaque to the server, so directory read + * only covers skills served as individual files. + */ + directoryRead?: boolean; +} diff --git a/typescript/sdk/src/uri.test.ts b/typescript/sdk/src/uri.test.ts new file mode 100644 index 0000000..3f892d4 --- /dev/null +++ b/typescript/sdk/src/uri.test.ts @@ -0,0 +1,251 @@ +import { describe, it, expect } from "vitest"; +import { + parseSkillUri, + resolveSkillFileUri, + buildSkillUri, + isSkillContentUri, + isIndexJsonUri, + isValidSkillName, + extractSkillPathFromUri, + SKILL_URI_SCHEME, + INDEX_JSON_URI, +} from "./uri.js"; + +// --------------------------------------------------------------------------- +// parseSkillUri +// --------------------------------------------------------------------------- + +describe("parseSkillUri", () => { + it("parses single-segment SKILL.md", () => { + expect(parseSkillUri("skill://code-review/SKILL.md")).toEqual({ + skillPath: "code-review", + filePath: "SKILL.md", + }); + }); + + it("parses multi-segment SKILL.md", () => { + expect(parseSkillUri("skill://acme/billing/refunds/SKILL.md")).toEqual({ + skillPath: "acme/billing/refunds", + filePath: "SKILL.md", + }); + }); + + it("handles case-insensitive skill.md", () => { + const result = parseSkillUri("skill://my-skill/skill.md"); + expect(result).toEqual({ skillPath: "my-skill", filePath: "skill.md" }); + }); + + it("returns null for non-skill URIs", () => { + expect(parseSkillUri("https://example.com/foo")).toBeNull(); + expect(parseSkillUri("file:///tmp/SKILL.md")).toBeNull(); + expect(parseSkillUri("")).toBeNull(); + }); + + it("returns null for the well-known index URI", () => { + expect(parseSkillUri(INDEX_JSON_URI)).toBeNull(); + }); + + it("returns null for bare scheme with no path", () => { + expect(parseSkillUri("skill://")).toBeNull(); + }); + + it("returns empty skillPath for arbitrary supporting files", () => { + const result = parseSkillUri("skill://acme/billing/refunds/templates/email.md"); + expect(result).toEqual({ + skillPath: "", + filePath: "acme/billing/refunds/templates/email.md", + }); + }); +}); + +// --------------------------------------------------------------------------- +// resolveSkillFileUri +// --------------------------------------------------------------------------- + +describe("resolveSkillFileUri", () => { + const knownPaths = ["code-review", "acme/billing/refunds", "acme/onboarding"]; + + it("resolves supporting file with longest-prefix match", () => { + expect( + resolveSkillFileUri( + "skill://acme/billing/refunds/templates/email.md", + knownPaths, + ), + ).toEqual({ + skillPath: "acme/billing/refunds", + filePath: "templates/email.md", + }); + }); + + it("resolves single-segment skill supporting file", () => { + expect( + resolveSkillFileUri( + "skill://code-review/references/GUIDE.md", + knownPaths, + ), + ).toEqual({ + skillPath: "code-review", + filePath: "references/GUIDE.md", + }); + }); + + it("returns null for unknown skill path", () => { + expect( + resolveSkillFileUri("skill://unknown/foo.md", knownPaths), + ).toBeNull(); + }); + + it("returns null for non-skill URIs", () => { + expect(resolveSkillFileUri("https://example.com", knownPaths)).toBeNull(); + }); + + it("prefers longer prefix when paths overlap", () => { + const paths = ["acme", "acme/billing", "acme/billing/refunds"]; + expect( + resolveSkillFileUri( + "skill://acme/billing/refunds/doc.md", + paths, + ), + ).toEqual({ + skillPath: "acme/billing/refunds", + filePath: "doc.md", + }); + }); +}); + +// --------------------------------------------------------------------------- +// buildSkillUri +// --------------------------------------------------------------------------- + +describe("buildSkillUri", () => { + it("defaults to SKILL.md", () => { + expect(buildSkillUri("code-review")).toBe("skill://code-review/SKILL.md"); + }); + + it("builds multi-segment SKILL.md URI", () => { + expect(buildSkillUri("acme/billing/refunds")).toBe( + "skill://acme/billing/refunds/SKILL.md", + ); + }); + + it("builds supporting file URI", () => { + expect(buildSkillUri("code-review", "references/GUIDE.md")).toBe( + "skill://code-review/references/GUIDE.md", + ); + }); +}); + +// --------------------------------------------------------------------------- +// Type-check helpers +// --------------------------------------------------------------------------- + +describe("URI type checks", () => { + it("isSkillContentUri identifies SKILL.md URIs", () => { + expect(isSkillContentUri("skill://code-review/SKILL.md")).toBe(true); + expect(isSkillContentUri("skill://acme/billing/refunds/SKILL.md")).toBe(true); + expect(isSkillContentUri("skill://x/skill.md")).toBe(true); + expect(isSkillContentUri("skill://code-review/references/foo.md")).toBe(false); + expect(isSkillContentUri(INDEX_JSON_URI)).toBe(false); + }); + + it("isIndexJsonUri identifies index.json", () => { + expect(isIndexJsonUri(INDEX_JSON_URI)).toBe(true); + expect(isIndexJsonUri("skill://index.json/SKILL.md")).toBe(false); + expect(isIndexJsonUri("skill://foo/index.json")).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// Round-trip: build → parse +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// isValidSkillName (Agent Skills naming rule) +// --------------------------------------------------------------------------- + +describe("isValidSkillName", () => { + it("accepts lowercase letters, digits, and hyphens", () => { + expect(isValidSkillName("git-workflow")).toBe(true); + expect(isValidSkillName("refunds")).toBe(true); + expect(isValidSkillName("v2-api")).toBe(true); + expect(isValidSkillName("abc123")).toBe(true); + }); + + it("rejects uppercase, underscore, dot, slash, space", () => { + expect(isValidSkillName("MyCoolSkill")).toBe(false); + expect(isValidSkillName("git_workflow")).toBe(false); + expect(isValidSkillName("foo.bar")).toBe(false); + expect(isValidSkillName("foo/bar")).toBe(false); + expect(isValidSkillName("foo bar")).toBe(false); + }); + + it("rejects empty string", () => { + expect(isValidSkillName("")).toBe(false); + }); + + it("rejects index.json (justifies the SEP reservation)", () => { + expect(isValidSkillName("index.json")).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// extractSkillPathFromUri (cross-scheme path extraction) +// --------------------------------------------------------------------------- + +describe("extractSkillPathFromUri", () => { + it("extracts path from skill:// URIs", () => { + expect(extractSkillPathFromUri("skill://git-workflow/SKILL.md")).toBe( + "git-workflow", + ); + expect( + extractSkillPathFromUri("skill://acme/billing/refunds/SKILL.md"), + ).toBe("acme/billing/refunds"); + }); + + it("extracts path from non-skill schemes (authority included)", () => { + expect( + extractSkillPathFromUri( + "github://owner/repo/skills/refunds/SKILL.md", + ), + ).toBe("owner/repo/skills/refunds"); + expect( + extractSkillPathFromUri( + "repo://github/awesome-copilot/contents/skills/copilot-sdk/SKILL.md", + ), + ).toBe("github/awesome-copilot/contents/skills/copilot-sdk"); + }); + + it("matches case-insensitively on the SKILL.md filename", () => { + expect(extractSkillPathFromUri("skill://x/skill.md")).toBe("x"); + expect(extractSkillPathFromUri("skill://x/Skill.MD")).toBe("x"); + }); + + it("returns null for URIs that don't end in SKILL.md", () => { + expect( + extractSkillPathFromUri("skill://x/references/GUIDE.md"), + ).toBeNull(); + expect(extractSkillPathFromUri("skill://pdf-processing.tar.gz")).toBeNull(); + }); + + it("returns null for non-URI strings", () => { + expect(extractSkillPathFromUri("not-a-uri")).toBeNull(); + expect(extractSkillPathFromUri("")).toBeNull(); + expect(extractSkillPathFromUri("/just/a/path/SKILL.md")).toBeNull(); + }); +}); + +describe("round-trip", () => { + const paths = ["git-workflow", "acme/billing/refunds", "a/b/c/d"]; + + for (const sp of paths) { + it(`build → parse for "${sp}"`, () => { + const uri = buildSkillUri(sp); + const parsed = parseSkillUri(uri); + expect(parsed).toEqual({ skillPath: sp, filePath: "SKILL.md" }); + }); + } + + it("scheme constant is correct", () => { + expect(SKILL_URI_SCHEME).toBe("skill://"); + }); +}); diff --git a/typescript/sdk/src/uri.ts b/typescript/sdk/src/uri.ts new file mode 100644 index 0000000..8fb87b3 --- /dev/null +++ b/typescript/sdk/src/uri.ts @@ -0,0 +1,183 @@ +/** + * URI parsing and building utilities for skill:// URIs. + * + * Supports multi-segment skill paths per the Skills Extension SEP: + * - skill://code-review/SKILL.md (single-segment) + * - skill://acme/billing/refunds/SKILL.md (multi-segment) + * - skill://acme/billing/refunds/templates/email.md (supporting file) + * + * Per the SEP: the final segment of MUST equal the skill's + * frontmatter name. Preceding segments are a server-chosen organizational + * prefix. In skill://acme/billing/refunds/SKILL.md, the prefix is + * "acme/billing" and the skill name is "refunds". + */ + +/** The skill:// URI scheme prefix. */ +export const SKILL_URI_SCHEME = "skill://"; + +/** Default skill content filename. */ +export const SKILL_FILENAME = "SKILL.md"; + +/** Well-known URI for the skill index (SEP discovery mechanism). */ +export const INDEX_JSON_URI = "skill://index.json"; + +/** + * Agent Skills naming rule: skill names contain only lowercase letters, + * digits, and hyphens. Per SEP-2640, the final segment of `` — + * which equals the frontmatter `name` — MUST satisfy this rule. The rule + * also underpins the SEP's reservation note that `index.json` cannot + * collide with a skill name. + */ +const SKILL_NAME_REGEX = /^[a-z0-9-]+$/; + +/** + * Check whether a string satisfies the Agent Skills `name` field rule: + * lowercase letters, digits, and hyphens, non-empty. + */ +export function isValidSkillName(name: string): boolean { + return SKILL_NAME_REGEX.test(name); +} + +/** + * Whether a final path segment names a skill's content file. The canonical + * spelling is `SKILL.md` (the {@link SKILL_FILENAME} sentinel); we also accept + * any case variant since some filesystems/servers are case-insensitive. + */ +export function isSkillContentFilename(segment: string): boolean { + return segment === SKILL_FILENAME || segment.toLowerCase() === "skill.md"; +} + +/** + * Parsed components of a skill:// URI. + */ +export interface ParsedSkillUri { + /** Multi-segment skill path (e.g., "acme/billing/refunds") */ + skillPath: string; + /** File path within the skill (e.g., "SKILL.md", "templates/email.md") */ + filePath: string; +} + +/** + * Parse a skill:// URI into skill path and file path components. + * + * For SKILL.md URIs, the split is unambiguous because the last segment is + * a known sentinel. For supporting file URIs, the caller must use + * resolveSkillFileUri() with known skill paths. + * + * Returns null if the URI doesn't match the skill:// scheme or is the + * special index.json URI. + * + * Examples: + * "skill://code-review/SKILL.md" + * → { skillPath: "code-review", filePath: "SKILL.md" } + * "skill://acme/billing/refunds/SKILL.md" + * → { skillPath: "acme/billing/refunds", filePath: "SKILL.md" } + */ +export function parseSkillUri(uri: string): ParsedSkillUri | null { + if (!uri.startsWith(SKILL_URI_SCHEME)) return null; + + const rest = uri.slice(SKILL_URI_SCHEME.length); + if (!rest || rest === "index.json") return null; + + const slashIndex = rest.lastIndexOf("/"); + if (slashIndex === -1) return null; + + const beforeLast = rest.slice(0, slashIndex); + const afterLast = rest.slice(slashIndex + 1); + + // Known sentinel: SKILL.md as the last segment + if (isSkillContentFilename(afterLast)) { + return { skillPath: beforeLast, filePath: afterLast }; + } + + // For arbitrary file paths, we can't determine the split from the URI alone. + // Return with empty skillPath — caller should use resolveSkillFileUri(). + return { skillPath: "", filePath: rest }; +} + +/** + * Resolve a skill:// URI for a supporting file by matching against known + * skill paths. Uses longest-prefix matching to handle nested hierarchies. + * + * Example: + * resolveSkillFileUri( + * "skill://acme/billing/refunds/templates/email.md", + * ["code-review", "acme/billing/refunds", "acme/onboarding"] + * ) + * → { skillPath: "acme/billing/refunds", filePath: "templates/email.md" } + */ +export function resolveSkillFileUri( + uri: string, + knownSkillPaths: string[], +): ParsedSkillUri | null { + if (!uri.startsWith(SKILL_URI_SCHEME)) return null; + + const rest = uri.slice(SKILL_URI_SCHEME.length); + + // Sort by length descending for longest-prefix match + const sorted = [...knownSkillPaths].sort((a, b) => b.length - a.length); + for (const sp of sorted) { + if (rest.startsWith(sp + "/")) { + return { skillPath: sp, filePath: rest.slice(sp.length + 1) }; + } + } + + return null; +} + +/** + * Build a skill:// URI from a multi-segment skill path and optional file path. + * Defaults to SKILL.md if no file path is provided. + * + * Examples: + * buildSkillUri("acme/billing/refunds") + * → "skill://acme/billing/refunds/SKILL.md" + * buildSkillUri("code-review", "references/REFERENCE.md") + * → "skill://code-review/references/REFERENCE.md" + */ +export function buildSkillUri(skillPath: string, filePath?: string): string { + return `${SKILL_URI_SCHEME}${skillPath}/${filePath ?? SKILL_FILENAME}`; +} + +/** + * Check if a URI points to a skill's SKILL.md content. + */ +export function isSkillContentUri(uri: string): boolean { + const parsed = parseSkillUri(uri); + return parsed !== null && isSkillContentFilename(parsed.filePath); +} + +/** + * Check if a URI is the well-known skill index resource. + */ +export function isIndexJsonUri(uri: string): boolean { + return uri === INDEX_JSON_URI; +} + +/** + * Extract the `` from any-scheme URI ending in `/SKILL.md`. + * + * Per SEP-2640, the structural constraints on `` (final segment + * equals the skill name, `SKILL.md` explicit, no nesting) apply regardless + * of scheme. So for `github://owner/repo/skills/refunds/SKILL.md` the + * skill-path is `owner/repo/skills/refunds`. + * + * Returns null if the URI doesn't have the form `:///SKILL.md` + * (case-insensitive on the filename). + */ +export function extractSkillPathFromUri(uri: string): string | null { + const schemeMatch = uri.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(.*)$/); + if (!schemeMatch) return null; + + const rest = schemeMatch[1]; + const slashIndex = rest.lastIndexOf("/"); + if (slashIndex <= 0) return null; + + const lastSegment = rest.slice(slashIndex + 1); + if (!isSkillContentFilename(lastSegment)) { + return null; + } + + const skillPath = rest.slice(0, slashIndex); + return skillPath || null; +} diff --git a/typescript/sdk/src/xml.ts b/typescript/sdk/src/xml.ts new file mode 100644 index 0000000..bdf82eb --- /dev/null +++ b/typescript/sdk/src/xml.ts @@ -0,0 +1,63 @@ +/** + * XML generation for the client-side system-prompt skills catalog. + */ + +import type { SkillSummary } from "./types.js"; + +/** + * Escape XML special characters. + */ +export function escapeXml(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * Options for generateSkillsXMLFromSummaries(). + */ +export interface SkillsXmlOptions { + /** Server name to inject when `serverInEntries` is true. */ + serverName?: string; + /** + * Inject `{serverName}` into each `` entry. + * Default: false. The host SKILL.md flags this as a way to keep + * first-call activation reliability ~90% for `(server, uri)` reader + * tools — but it's not in SEP-2640, so the SDK leaves it off by default. + */ + serverInEntries?: boolean; +} + +/** + * Generate XML from a client-side SkillSummary array. + */ +export function generateSkillsXMLFromSummaries( + skills: SkillSummary[], + options?: SkillsXmlOptions, +): string { + const serverName = options?.serverName; + const inEntries = options?.serverInEntries === true; + const lines: string[] = [""]; + + for (const skill of skills) { + lines.push(" "); + lines.push(` ${escapeXml(skill.name)}`); + lines.push(` ${escapeXml(skill.skillPath)}`); + if (serverName && inEntries) { + lines.push(` ${escapeXml(serverName)}`); + } + if (skill.description) { + lines.push( + ` ${escapeXml(skill.description)}`, + ); + } + lines.push(` ${escapeXml(skill.uri)}`); + lines.push(" "); + } + + lines.push(""); + return lines.join("\n"); +} diff --git a/typescript/sdk/tsconfig.json b/typescript/sdk/tsconfig.json new file mode 100644 index 0000000..cfc52f2 --- /dev/null +++ b/typescript/sdk/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/typescript/sdk/vitest.config.ts b/typescript/sdk/vitest.config.ts new file mode 100644 index 0000000..e2ec332 --- /dev/null +++ b/typescript/sdk/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + }, +});