Soklet Logo

Model Context Protocol

MCP Skills

A Skill packages instructions in SKILL.md with supporting files that a client can retrieve when needed. Soklet implements the io.modelcontextprotocol/skills extension: skills/list discovers manifests, skills/get retrieves one manifest, and resources/read serves its files. Your application supplies the content; Soklet validates and snapshots it, generates the manifest, and handles retrieval.

Publishing a Skill does not run its instructions, execute bundled scripts, activate anything in a host, or grant permission to call tools. Those decisions belong to the client and the application's existing authorization policy.

Skills registrations and custom list handlers currently require exactly McpProtocolVersion.V2026_07_28. An endpoint may also declare the supported 2025 revisions for ordinary tools, prompts, and resources. On the same URL, those legacy views omit generated Skill manifests and files; even a known file URI cannot be read through the legacy view. See Compatibility and Scope.

Publish a bundle

Supply a map of logical bundle-relative paths to complete file bytes. The root SKILL.md needs YAML frontmatter containing a valid name and description:

byte[] instructions = """
  ---
  name: catalog-guide
  description: Help a user explore the read-only toy catalog.
  ---

  # Catalog guide

  Ask what kind of toy the user wants before looking up catalog entries.
  Consult references/fields.md to interpret the returned values.
  Do not infer availability or place an order from this guide.
  """.getBytes(StandardCharsets.UTF_8);

byte[] fields = """
  # Catalog fields

  A price is an exact decimal with an explicit currency code.
  Catalog data is descriptive; it does not authorize a purchase.
  """.getBytes(StandardCharsets.UTF_8);

McpSkillBundle bundle = McpSkillBundle.fromFiles(Map.of(
  "SKILL.md", instructions,
  "references/fields.md", fields
));

McpSkillRegistration catalogGuide = McpSkillRegistration
  .withUriAndSkillBundle(
    URI.create("skill://catalog.example/catalog-guide/SKILL.md"), bundle,
    Set.of(McpProtocolVersion.V2026_07_28))
  .build();

McpEndpoint endpoint = McpEndpoint.withPath("/mcp",
    McpImplementation.withNameAndVersion("catalog", "1.0").build(),
    Set.of(McpProtocolVersion.V2026_07_28))
  .skillRegistrations(List.of(catalogGuide))
  .build();

Use com.soklet.McpSkillBundle, McpSkillRegistration, McpEndpoint, and McpImplementation, plus java.net.URI, java.nio.charset.StandardCharsets, java.util.List, java.util.Map, and java.util.Set. Attach the endpoint with McpEndpointRegistry.fromEndpoints(List.of(endpoint)) as described in Endpoint Authoring.

The registration URI identifies the root document, not a directory. Its final directory segment must match the authored name, and it must end in literal /SKILL.md. Soklet derives the supporting URI skill://catalog.example/catalog-guide/references/fields.md from the bundle path. These are MCP resource identifiers, not URLs Soklet fetches or filesystem paths.

McpSkillBundle owns an immutable snapshot of the supplied bytes. The generated manifest contains each file's URI, SHA-256 digest, and raw byte size; you do not maintain a separate manifest by hand. Original bytes and line endings are preserved. Metadata includes unknown frontmatter fields as immutable JSON-compatible values, so application-specific nested metadata survives discovery.

Load classpath resources, read reviewed files, or generate bytes in application code before constructing the bundle. Soklet does not open files, watch directories, fetch remote content, or reload a bundle automatically. Logical paths must stay within the bundle; review any filesystem loading policy in your application separately. Common text formats use UTF-8 resource contents when valid; binary and unrecognized formats use base64 resource contents.

File paths must be distinct NFC logical names. A file cannot also be a directory prefix of another file: ref and ref/notes.md, or SKILL.md and SKILL.md/notes.md, fail construction before root parsing or byte copying. Logical names retain exact, case-sensitive spelling. When loading or materializing files on a case-insensitive filesystem, account for names such as README.md and readme.md that collide there.

Invalid metadata produces an IllegalArgumentException naming the fixed field and violated rule, for example Skills field 'description' exceeds 1024 code points. Messages omit authored values and custom metadata keys. Soklet keeps the existing Agent Skills limits and does not repair or rewrite the document; these diagnostics are not a structured validation API. A construction failure inside a request callback remains a fixed private internal error for the client.

The runnable standalone example includes a root document, a UTF-8 CSV, and a small binary asset. Its Inspector instructions demonstrate retrieval and integrity verification, not host activation.

Add Skills to an annotated endpoint

Skills are registered programmatically; there is no @McpSkill annotation. You can retain an existing annotated endpoint's generated tools, prompts, resources, and settings, and attach Skills through its registry:

McpEndpointRegistry registry = McpEndpointRegistry
  .fromClasses(CatalogEndpoint.class)
  .withSkillRegistrations(CatalogEndpoint.class, List.of(catalogGuide));

Here CatalogEndpoint is your annotated class and catalogGuide is the registration above. Supply this registry to McpServer.Builder.endpointRegistry. The immutable wither replaces that endpoint's complete standalone Skills list; an empty list clears it. It requires the exact class loaded into the registry, preserves endpoint order and generated handler metadata, and does not acquire an endpoint instance. Keep the returned registry: the original is unchanged. Do not rebuild a new endpoint at the same path or append a duplicate endpoint just to add Skills.

For locale alternatives, register explicitly authored bundles in a group and attach it with withSkillGroups(...) instead:

McpSkillRegistration englishGuide = McpSkillRegistration
  .withUriAndSkillBundle(
    URI.create("skill://catalog.example/v1/en-US/catalog-guide/SKILL.md"), bundle,
    Set.of(McpProtocolVersion.V2026_07_28))
  .locale(Locale.US)
  .build();

McpSkillRegistration germanGuide = McpSkillRegistration
  .withUriAndSkillBundle(
    URI.create("skill://catalog.example/v1/de-DE/catalog-guide/SKILL.md"),
    McpSkillBundle.fromFiles(germanFiles),
    Set.of(McpProtocolVersion.V2026_07_28))
  .locale(Locale.GERMANY)
  .build();

McpSkillGroup catalogGuideVariants = McpSkillGroup.fromKeyAndSkillRegistrations(
  "catalog-guide", List.of(englishGuide, germanGuide));

McpEndpointRegistry localizedRegistry = McpEndpointRegistry
  .fromClasses(CatalogEndpoint.class)
  .withSkillGroups(CatalogEndpoint.class, List.of(catalogGuideVariants));

Here bundle is the English snapshot above, and germanFiles is a Map<String, byte[]> containing the separately authored German files with the same name: catalog-guide. Also import com.soklet.McpSkillGroup and java.util.Locale. Configure the server's skillVariantSelector(...) to select one supplied variant for discovery; see Locales, catalogs, and caching.

withSkillGroups(...) replaces the complete group list; an empty list clears groups, preserving standalone registrations. Both registry methods reject null, preserve the generated endpoint and its settings, and validate collisions before returning the new registry. A name cannot occupy both a standalone registration and a group, so do not attach both versions of catalog-guide to one endpoint.

The Toy Store application uses a group to publish English, German, and Brazilian Portuguese catalog guides without replacing its existing annotated tools and resources.

Discovery and file retrieval

A client advertises io.modelcontextprotocol/skills in its per-request clientCapabilities.extensions. For the standalone public endpoint above, running locally on port 8082, list its Skills with:

curl --fail-with-body --silent --show-error \
  --request POST http://127.0.0.1:8082/mcp \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json, text/event-stream' \
  --header 'MCP-Protocol-Version: 2026-07-28' \
  --header 'Mcp-Method: skills/list' \
  --data '{"jsonrpc":"2.0","id":1,"method":"skills/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{"extensions":{"io.modelcontextprotocol/skills":{}}}}}}'

Add the application's authorization header for a protected endpoint. For skills/get, change both method occurrences to skills/get and add "uri":"skill://catalog.example/catalog-guide/SKILL.md" to params. Both Skills methods omit Mcp-Name. To retrieve an actual file, use resources/read, put its exact URI in params.uri, and set Mcp-Name to that same URI. Keep the required protocol metadata on every request.

A Skills-only endpoint advertises the base Resources capability so clients can read its files. Those files do not automatically appear in resources/list or resources/templates/list; their manifest is the discovery source. Ordinary resources can coexist, but an exact registration or matching URI template cannot shadow a Skill file.

Access is separate from discovery

Configure admission to authenticate each MCP request. The server's McpSkillAccessPolicy then separates two decisions:

  • Access: may this admitted caller retrieve this registration and its files?
  • Discovery: should an accessible registration appear in skills/list?

Configure McpServer.Builder.skillAccessPolicy(...) with McpSkillAccessPolicy.fromEvaluators(accessEvaluator, discoveryEvaluator) when different callers should see different content. Both callbacks receive the admitted request context, canonical registration, and invocation features, and must support concurrent calls. The default allows every admitted caller access to and discovery of every registered Skill; it does not add authentication.

Listing checks access first. A hidden but accessible Skill remains available by exact URI through skills/get and resources/read. Each later lookup or read checks access again: possessing a URI, receiving an earlier manifest, or having a cached copy is not a new authorization grant. Denied and unknown targets produce neutral unavailable errors. Callback failure or a null result fails closed.

If bundles share a file, access through any owning registration can grant the read. An accessible parent bundle also exposes the bytes it includes from a nested Skill, even if the nested registration is separately denied. Do not put restricted content inside a bundle available to a broader audience. A nested SKILL.md is only supporting content unless separately registered.

Authored instructions are application content. Review them and their supporting files like any other content a model will consume. Skills retrieval does not bypass tool authorization, and digest verification establishes file integrity, not that the instructions are safe to follow.

Locales, catalogs, and caching

Author translated bundles explicitly; localization never rewrites a Skill's bytes or hashes. Register alternatives with McpSkillGroup.fromKeyAndSkillRegistrations(key, registrations) and the endpoint builder's skillGroups(...), or the registry's withSkillGroups(...) for an annotated endpoint. Group members share one Skill name and have distinct URIs and locales. Configure McpServer.Builder.skillVariantSelector(...) for a group with multiple variants. The selector receives only accessible, discoverable candidates plus the bounded client language preferences; it returns one exact supplied instance or an empty optional. The application owns language matching, zero-weight exclusions, and fallback. Selection affects listing, not authorization or direct URI lookup. See Localization.

For a small catalog, Soklet produces one automatic page of at most 32 complete Skill manifests. A larger catalog uses the endpoint builder's skillListHandler(...), returning McpSkillPage directly. The first page must use an ordered subset of the supplied initial selection; continuation cursors and retention of that selection are application-owned. Bind opaque cursors to the caller, endpoint, content snapshot, and expiry, and recheck current access. Pagination cannot make an oversized individual file or manifest fit.

The default protocol cache policy is private with zero TTL. Caller-dependent access, locale selection, or custom listing conservatively keeps Skills responses private and immediately stale. HTTP responses remain Cache-Control: no-store independently of those protocol cache hints. Start with these defaults; a cache policy never replaces authorization.

Previous
Prompts and Resources