npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@olaservo/ext-skills

v0.12.1

Published

SDK for the Skills Extension SEP

Downloads

511

Readme

@olaservo/ext-skills

TypeScript SDK for SEP-2640 v1 (Skills Extension) — serves agent skills as skill:// resources over MCP, with skills/list / skills/get entry retrieval, per-file digest verification, and optional directory enumeration. Built on the v2 MCP TypeScript SDK (@modelcontextprotocol/server / @modelcontextprotocol/client).

Experimental. Published under a personal scope for testing while SEP-2640 is in review; the protocol surface may change with the SEP. In-repo examples consume the same code as @modelcontextprotocol/experimental-ext-skills via a file: dependency.

Install

# Server-side
npm install @olaservo/ext-skills @modelcontextprotocol/server

# Client-side
npm install @olaservo/ext-skills @modelcontextprotocol/client

Subpath exports

| Import path | Purpose | |---|---| | @olaservo/ext-skills | Shared types, protocol method schemas, URI utilities, constants | | @olaservo/ext-skills/server | Server-side: discover skills, register resources + skills/list / skills/get handlers | | @olaservo/ext-skills/client | Client-side: list/get entries, verified reads, catalogs, directory enumeration |

Protocol surface (SEP-2640 v1)

Every server declaring the io.modelcontextprotocol/skills extension implements two methods:

  • skills/list — paginated enumeration of skill entries. Each entry carries the skill's uri, its verbatim SKILL.md frontmatter as JSON, and a complete resources manifest: {uri, digest} for SKILL.md and every supporting file. The listing MAY be empty or partial (large/generated/unenumerable catalogs); hosts MUST NOT treat that as proof a server has no skills. In protocol 2026-07-28+ the result also carries the SEP-2549 list-caching attributes (ttlMs, cacheScope).
  • skills/get — returns the entry for one skill by the URI of its SKILL.md, whether or not it appears in the listing; errors -32602 for URIs the server does not serve as skills. This is both how unlisted skills get verified and how a host confirms an explicitly referenced URI is a skill (never by inspecting the URI scheme).

One optional method, gated behind the directoryRead capability setting:

  • resources/directory/readls-style, metadata-only, paginated listing of a directory resource's direct children; directories carry mimeType: "inode/directory".

A skill is always retrieved as individually addressable resources via resources/read — the SEP defines no packed or bundled retrieval form (archive distribution was removed during core-maintainer review; see the SEP's "Appendix: Deferred Features").

Server usage

Discover skills from a directory of SKILL.md files and serve them:

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import {
  discoverSkills,
  registerSkillResources,
} from "@olaservo/ext-skills/server";

// Recursively scan a directory for SKILL.md files (per-file SHA-256 digests
// are computed here, once).
const skillMap = discoverSkills("./skills");

const server = new McpServer(
  { name: "my-server", version: "1.0.0" },
  { capabilities: { resources: {} } },
);

// Registers the skill resources, the skills/list + skills/get handlers, the
// optional resources/directory/read handler, and declares
// capabilities.extensions["io.modelcontextprotocol/skills"] — all before
// connect(), because capabilities ship in the initialize handshake.
registerSkillResources(server, skillMap, "./skills", {
  template: true,        // catch-all resource template for supporting files
  directoryRead: true,   // implement resources/directory/read + declare the setting
  ttlMs: 60_000,         // SEP-2549 freshness hint on skills/list results
  cacheScope: "public",  // safe only when the catalog has no user-specific data
  // ttlMs/cacheScope are emitted only on 2026-07-28+ connections — the SEP
  // scopes the list-caching attributes to those protocol versions, and the
  // handler detects the version from each request's _meta envelope.
  // 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());

registerSkillResources declares the extension capability itself (pass declareCapability: false and call declareSkillsExtension(server.server, …) yourself if you need manual control). Declaring the extension commits the server to skills/list and skills/get; clients MUST NOT call resources/directory/read unless directoryRead: true was declared.

Protocol versions (2025 eras and 2026-07-28)

The SDK works on every protocol version the v2 MCP SDK speaks; which one a connection uses is decided by the transport entry points, not by this SDK. The skills/list, skills/get, and resources/directory/read methods work identically on both eras. The one version-dependent behavior is SEP-2640's scoping of the SEP-2549 list-caching attributes: skills/list results carry ttlMs/cacheScope only on 2026-07-28+ connections (detected per request from the _meta envelope), and omit them on 2025-era connections. On 2026-07-28 connections the extension capability reaches clients via server/discover instead of the initialize result; serverSupportsSkills() / serverSupportsDirectoryRead() read it the same way either way.

To serve the 2026-07-28 revision (while still accepting 2025-era clients), build the server in a factory passed to the v2 SDK's era-aware entry points instead of calling connect() yourself:

import { createMcpHandler, McpServer } from "@modelcontextprotocol/server"; // HTTP
import { serveStdio } from "@modelcontextprotocol/server/stdio";            // stdio

serveStdio(() => {
  const server = new McpServer({ name: "my-server", version: "1.0.0" }, { capabilities: { resources: {} } });
  registerSkillResources(server, skillMap, "./skills", { directoryRead: true });
  return server;
});

Client-side, negotiate the newest revision the server offers with versionNegotiation: { mode: "auto" } (or pin with mode: { pin: "2026-07-28" }):

const client = new Client(
  { name: "my-client", version: "1.0.0" },
  { versionNegotiation: { mode: "auto" } },
);

See the reference examples for a full server/client pair running on 2026-07-28 over stdio.

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:

---
name: code-review
description: Review code changes for quality and correctness
---

# Code Review

Instructions for the agent...

Per the SEP, the final segment of the skill path MUST equal the frontmatter name (the SDK validates this and skips violators), and skills MAY nest: a SKILL.md in a descendant directory of another skill is discovered as a skill in its own right, while its files remain ordinary supporting content of the enclosing skill (the enclosing entry's resources lists them too).

What gets registered

  • skill://{skillPath}/SKILL.md — one listed resource per discovered skill
  • skill://{+skillFilePath} — catch-all resource template for supporting files (optional, on by default)
  • skills/list and skills/get request handlers (always)
  • A resources/directory/read handler when directoryRead: true

The entry served for each skill is built by buildSkillEntry(skill) — exported for servers that assemble their own handlers (makeSkillsListHandler / makeSkillsGetHandler / makeDirectoryReadHandler are exported too).

Snapshot serving

discoverSkills() captures every file's bytes (and digest) once, and registered resources serve that snapshot rather than re-reading disk. This is what keeps the server conformant with the SEP's identity requirements: the entry's digests and frontmatter always describe exactly the bytes resources/read returns, even if a file changes on disk while the server runs. On-disk edits take effect by re-running discoverSkills() and re-registering (before connect()) — typically a server restart. Files larger than 1MB are skipped with a logged warning: they appear neither in the resources manifest nor as readable resources, so the manifest stays complete with respect to what is actually served. Empty subdirectories are tracked too, so resources/directory/read lists them as empty rather than erroring.

Partial listings

Per the SEP, a listing MAY be partial: a server can serve skills it does not enumerate. Mark a skill listed: false to omit it from skills/list while skills/get continues to answer for it and its resources remain readable:

const skillMap = discoverSkills("./skills");
skillMap.get("internal/experimental-skill")!.listed = false;
registerSkillResources(server, skillMap, "./skills");

Resource annotations

All resources include annotations with audience, priority, and lastModified (see skill-meta-keys.md):

  • audience defaults to ["assistant"]. Override globally via options, or per-skill via SkillMetadata.audience.
  • priority is set per resource type: 1.0 (SKILL.md), 0.2 (supporting-file catch-all)
  • lastModified uses per-skill mtime for SKILL.md and the most recent mtime across all skills for the catch-all template
  • size is set on all resources except the catch-all template (which varies per request)

Custom _meta per skill

Per 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, set it on the discovered SkillMetadata.meta:

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");

Client usage

Quick start

Discover skills and build a system prompt catalog in one call:

import { discoverAndBuildCatalog } from "@olaservo/ext-skills/client";

const { skills, catalog } = await discoverAndBuildCatalog(client, {
  serverName: "my-skills-server",
});

console.log(`Discovered ${skills.length} skill(s)`);
// `skills` are SkillEntry objects — keep them; they are what reads verify against.
// Inject `catalog` into your agent's system prompt.

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 <server> inside every <skill> entry. Off by default because per-entry placement is host-implementation guidance, not in SEP-2640. Empirically lifts first-call activation ~33% → ~90% for (server, uri) reader tools.
  • Pass instructions: true to mine the server's instructions for skill URIs; each is confirmed via skills/get and merged with the listing (deduplicated by URI). Off by default.

Step by step

import {
  serverSupportsSkills,
  serverSupportsDirectoryRead,
  listSkills,
  getSkill,
  readSkill,
  readSkillResource,
  readSkillUri,
  readDirectory,
  walkDirectory,
  skillSummariesFromEntries,
  buildSkillsCatalog,
  buildSkillsSummary,
  verifyDigest,
  READ_RESOURCE_TOOL,
} from "@olaservo/ext-skills/client";

// Gate on the extension declaration (clients only issue skills/* calls
// after seeing it).
if (serverSupportsSkills(client)) {
  // Enumerate entries (paginates to exhaustion; MAY be empty or partial).
  const skills = await listSkills(client);

  // Retrieve one skill's entry by URI — listed or not. This is how a URI
  // from server instructions, another skill, or the user becomes a
  // verifiable entry. Errors -32602 for non-skill URIs.
  const entry = await getSkill(client, "skill://acme/billing/refunds/SKILL.md");

  // Verified SKILL.md read: checks the fetched bytes against the manifest
  // digest AND compares the parsed frontmatter field-by-field with the
  // entry's frontmatter (both host-side MUSTs). Throws on any mismatch.
  const content = await readSkill(client, entry);

  // Verified supporting-file read: the URI must be listed in the entry's
  // `resources` (an unlisted read is a verification failure), and the
  // content is checked against its digest.
  const doc = await readSkillResource(
    client,
    entry,
    "skill://acme/billing/refunds/templates/refund-email-template.md",
  );

  // Directory enumeration (only if the server declared the setting).
  if (serverSupportsDirectoryRead(client)) {
    const { resources } = await readDirectory(client, "skill://acme/billing/refunds");
    const allFiles = await walkDirectory(client, "skill://acme/billing/refunds");
  }

  // Catalog / summary for context injection.
  const summaries = skillSummariesFromEntries(skills);
  const catalog = buildSkillsCatalog(summaries, { toolName: "read_resource", serverName: "my-server" });
  const summary = buildSkillsSummary(summaries);
}

// Baseline: a URI alone is always enough to *read* a skill via
// resources/read, listed or not — pass a digest to verify when you hold one.
const raw = await readSkillUri(client, "skill://acme/billing/refunds/SKILL.md");

// READ_RESOURCE_TOOL — tool schema for model-driven skill loading.
console.log(READ_RESOURCE_TOOL);

Digests: verification and caching

Each entry's resources manifest carries a sha256:{hex} digest per file, and it serves two distinct purposes:

1. Verification — SEP-2640 makes this a MUST: when a host retrieves a file listed in a skill's resources, it must verify the content against that entry's digest, treat reads of unlisted files within the skill as verification failures, and (for SKILL.md) check the parsed frontmatter is identical to the entry's frontmatter. readSkill() and readSkillResource() do all of this by default. A mismatch means the content is not what the entry promised — corrupted, tampered, or stale because the skill changed; recover by calling getSkill() for a fresh entry (which, being different, revokes any content-bound approval) and retrying.

Digests are unsigned and come from the same server as the content: a match proves consistency, not trustworthiness. Hosts MUST NOT treat a digest match as a security boundary.

2. Caching — compare a fresh entry's digests against stored ones to decide whether cached content is still current, without re-reading files; or fetch, verify, and cache the entire set at approval time and serve every subsequent read from the verified copy:

// `cache` is your own Map<uri, digest> from a previous run.
const entry = await getSkill(client, skillUri);
for (const ref of entry.resources ?? []) {
  if (cache.get(ref.uri) === ref.digest) continue; // unchanged — skip refetch
  const doc = await readSkillResource(client, entry, ref.uri);
  cache.set(ref.uri, ref.digest);
  // ... (re)load content
}

Dynamically generated skills omit resources and are unverifiable by construction. readSkill() / readSkillResource() throw for them by default; pass { allowUnverified: true } to read anyway. Hosts MAY simply decline such skills.

SKILL.md is UTF-8, so hashing the received text (as UTF-8) matches the server's raw-byte hash exactly. Binary supporting files arrive as base64 blobs and are verified over the decoded bytes.

Scheme-agnostic skill identity

No URI scheme is privileged. A host learns that a resource is a skill from a skills/list entry or a skills/get answer — never from the URI scheme, skill:// included. Servers MAY serve skills under any scheme (github://, repo://, …); the structural constraints (path ends in the skill name, explicit SKILL.md) apply regardless, and all the client functions here are scheme-agnostic.

Server instructions as a pointer

A server MAY name specific skill URIs in its instructions. discoverSkills() / discoverAndBuildCatalog() accept { instructions: true } to mine client.getInstructions() for <scheme>://…SKILL.md URIs; each is confirmed via skills/get (the server answers for skills it serves and errors otherwise) and merged with the listing. Off by default — it costs one skills/get round-trip per URI mentioned. Pass extractor to override the built-in regex when the server uses a non-standard URI convention in prose:

const skills = await discoverSkills(client, {
  instructions: true,
  extractor: (text) => JSON.parse(text)["skills"] as string[],
});

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://acme/billing/refunds                     # directory resource (inode/directory)

URI utilities are available from the main import:

import { parseSkillUri, buildSkillUri, isSkillContentUri } from "@olaservo/ext-skills";

Related

License

Apache-2.0