Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ var client = McpClient.sync(transport)

### Pagination

`listTools`, `listResources`, `listResourceTemplates`, and `listPrompts` all accept an optional opaque `cursor` string, and their results carry a `nextCursor` that is non-null while more pages remain. Loop until `nextCursor` is `null` to collect every page:
`listTools`, `listResources`, `listResourceTemplates`, `listPrompts`, and `listSkills` all accept an optional opaque `cursor` string, and their results carry a `nextCursor` that is non-null while more pages remain. Loop until `nextCursor` is `null` to collect every page:

```java
List<McpSchema.Tool> allTools = new ArrayList<>();
Expand Down Expand Up @@ -542,6 +542,40 @@ Resources represent server-side data sources that clients can access using URI t
.subscribe();
```

### Skills Access

Servers that declare the `io.modelcontextprotocol/skills` extension expose Agent Skills as ordinary resources. After initialization, use `listSkills()` to discover the available skill entries and `getSkill(uri)` to retrieve the current entry for a known `SKILL.md` URI.

The entry contains the skill's `SKILL.md` URI, its verbatim frontmatter, and either a manifest of `SkillResource` values (URI, digest, and size) or `SkillResources.dynamicResources()`. Read `SKILL.md` and supporting files with the standard resource API; `readSkillUri(uri)` is a convenience for `readResource`:

=== "Sync API"

```java
ListSkillsResult skills = client.listSkills();
McpSchema.Skill skill = skills.skills().get(0);

// Retrieves a listed skill's current manifest, or a skill URI obtained elsewhere.
McpSchema.GetSkillResult result = client.getSkill(skill.uri());

// Skill files are regular resources, including SKILL.md.
ReadResourceResult skillInstructions = client.readSkillUri(result.skill().uri());
```

=== "Async API"

```java
client.listSkills()
.flatMap(skills -> client.getSkill(skills.skills().get(0).uri()))
.flatMap(skill -> client.readSkillUri(skill.skill().uri()))
.subscribe();
```

If the server declares `directoryRead: true` in this extension's capabilities, `readDirectory(uri)` lists all direct children of a directory resource. The SDK checks this capability before sending `resources/directory/read`:

```java
ListResourcesResult templates = client.readDirectory("skill://pdf-processing/templates");
```

### Resource Subscriptions

When the server advertises `resources.subscribe` support, clients can subscribe to individual resources and receive a callback whenever the server pushes a `notifications/resources/updated` notification for that URI. The SDK automatically re-reads the resource on notification and delivers the updated contents to the registered consumer.
Expand Down
57 changes: 57 additions & 0 deletions docs/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,63 @@ var binaryResourceSpecification = new McpServerFeatures.SyncResourceSpecificatio

`ReadResourceResult` accepts a list mixing `TextResourceContents` and `BlobResourceContents`, so a single resource read can return multiple representations if needed.

### Skills Specification

The Skills extension represents an Agent Skill as a `SKILL.md` resource plus an entry containing its frontmatter and a file manifest. It requires the normal `resources` capability and declares `io.modelcontextprotocol/skills` in `ServerCapabilities.extensions`. Set `directoryRead` to `true` only when clients may call `resources/directory/read`.

The stateless server API registers entries at build time with `skills(...)`, or later with `addSkill`. Register resource handlers for `SKILL.md` and every supporting file as well: clients retrieve their contents through the standard `resources/read` method. The following synchronous example advertises a static skill whose only file is `SKILL.md`:

```java
var skillUri = "skill://pdf-processing/SKILL.md";
var skillFile = new McpStatelessServerFeatures.SyncResourceSpecification(
Resource.builder(skillUri, "SKILL.md")
.mimeType("text/markdown")
.build(),
(exchange, request) -> ReadResourceResult.builder(List.of(
McpSchema.TextResourceContents.builder(skillUri,
"---\nname: pdf-processing\ndescription: Process PDF documents\n---\n")
.mimeType("text/markdown")
.build()))
.build());

var skill = new McpSchema.Skill(
skillUri,
McpSchema.SkillFrontmatter.of(Map.of(
"name", "pdf-processing",
"description", "Process PDF documents")),
McpSchema.SkillResources.manifest(List.of(
new McpSchema.SkillResource(skillUri,
"sha256:0ba2bf2df2af28aa6da64011be00042083a1b87bd0979a6d45e0b7bbf149e6a4",
64L))));

// A stateless server built with the capabilities below:
server.addResource(skillFile);
server.addSkill(skill);
```

Configure the capabilities when building that server:

```java
ServerCapabilities capabilities = ServerCapabilities.builder()
.resources(false, false)
.extensions(Map.of(
"io.modelcontextprotocol/skills", Map.of("directoryRead", true)))
.build();
```

`skills/list` returns every registered entry and `skills/get` retrieves one by its `SKILL.md` URI. A static manifest must contain every served file, including `SKILL.md`, with its digest and byte size. Use `SkillResources.dynamicResources()` when the skill's files are generated dynamically. When `directoryRead` is enabled, clients can list a skill directory's direct children.

For resources registered with `addResource` or `resources(...)`, the server derives directory resources and their direct children automatically. A generated or non-enumerable tree must provide its own directory listing handler; it receives the request URI and cursor and returns the corresponding page:

```java
McpStatelessAsyncServer server = McpServer.async(transport)
.serverInfo("my-server", "1.0.0")
.capabilities(capabilities)
.directoryReadHandler((context, request) ->
listGeneratedDirectory(request.uri(), request.cursor()))
.build();
```

### Resource Subscriptions

When the `subscribe` capability is enabled, clients can subscribe to specific resources and receive targeted `notifications/resources/updated` notifications when those resources change. Only sessions that have explicitly subscribed to a given URI receive the notification — not every connected client.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
import io.modelcontextprotocol.spec.McpSchema.ElicitUrlRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.GetSkillRequest;
import io.modelcontextprotocol.spec.McpSchema.GetSkillResult;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
import io.modelcontextprotocol.spec.McpSchema.ListSkillsResult;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
Expand Down Expand Up @@ -896,6 +899,63 @@ public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceReq
});
}

/**
* Reads a skill resource through the standard {@code resources/read} method.
* @param uri The skill resource URI, including a skill's {@code SKILL.md} URI
* @return A Mono that emits the resource content.
*/
public Mono<McpSchema.ReadResourceResult> readSkillUri(String uri) {
return this.readResource(McpSchema.ReadResourceRequest.builder(uri).build());
}

/**
* Lists every direct child of a directory resource. This method is available only
* when the server's Skills extension declares {@code directoryRead: true}.
* @param uri The directory resource URI
* @return A Mono that emits all direct children of the directory.
*/
public Mono<McpSchema.ListResourcesResult> readDirectory(String uri) {
return this.readDirectory(uri, McpSchema.FIRST_PAGE).expand(result -> {
String next = result.nextCursor();
return (next != null && !next.isEmpty()) ? this.readDirectory(uri, next) : Mono.empty();
}).reduce(new ArrayList<McpSchema.Resource>(), (accumulated, result) -> {
accumulated.addAll(result.resources());
return accumulated;
}).map(all -> McpSchema.ListResourcesResult.builder(Collections.unmodifiableList(all)).build());
}

/**
* Lists one page of direct children of a directory resource.
* @param uri The directory resource URI
* @param cursor Optional pagination cursor from a previous directory read
* @return A Mono that emits one page of directory children.
*/
public Mono<McpSchema.ListResourcesResult> readDirectory(String uri, String cursor) {
return this.readDirectory(uri, cursor, null);
}

/**
* Lists one page of direct children of a directory resource with optional metadata.
* @param uri The directory resource URI
* @param cursor Optional pagination cursor from a previous directory read
* @param meta Optional metadata to include in the request ({@code _meta} field)
* @return A Mono that emits one page of directory children.
*/
public Mono<McpSchema.ListResourcesResult> readDirectory(String uri, String cursor, Map<String, Object> meta) {
return this.initializer.withInitialization("reading resource directories", init -> {
if (init.initializeResult().capabilities().resources() == null) {
return Mono.error(new IllegalStateException("Server does not provide the resources capability"));
}
if (!init.initializeResult().capabilities().skillsDirectoryReadEnabled()) {
return Mono.error(
new IllegalStateException("Server does not declare Skills extension directoryRead capability"));
}
return init.mcpSession()
.sendRequest(McpSchema.METHOD_RESOURCES_DIRECTORY_READ,
new McpSchema.ReadDirectoryRequest(uri, cursor, meta), LIST_RESOURCES_RESULT_TYPE_REF);
});
}

/**
* Retrieves the list of all resource templates provided by the server. Resource
* templates allow servers to expose parameterized resources using URI templates,
Expand Down Expand Up @@ -1087,6 +1147,74 @@ private NotificationHandler asyncPromptsChangeNotificationHandler(
.then());
}

// --------------------------
// Skills Extension
// --------------------------
private static final TypeRef<McpSchema.ListSkillsResult> LIST_SKILLS_RESULT_TYPE_REF = new TypeRef<>() {
};

private static final TypeRef<McpSchema.GetSkillResult> GET_SKILL_RESULT_TYPE_REF = new TypeRef<>() {
};

/**
* Retrieves every skill exposed by a server supporting the Skills extension.
* @return A Mono that emits the complete list of skills.
*/
public Mono<ListSkillsResult> listSkills() {
return this.listSkills(McpSchema.FIRST_PAGE).expand(result -> {
String next = result.nextCursor();
return (next != null && !next.isEmpty()) ? this.listSkills(next) : Mono.empty();
}).reduce(new ArrayList<McpSchema.Skill>(), (accumulated, result) -> {
accumulated.addAll(result.skills());
return accumulated;
}).map(all -> McpSchema.ListSkillsResult.builder(Collections.unmodifiableList(all)).build());
}

/**
* Retrieves one page of skills exposed by a server supporting the Skills extension.
* @param cursor Optional pagination cursor from a previous list request
* @return A Mono that emits the page of skills.
*/
public Mono<ListSkillsResult> listSkills(String cursor) {
return this.listSkillsInternal(cursor, null);
}

/**
* Retrieves one page of skills, including optional request metadata.
* @param cursor Optional pagination cursor from a previous list request
* @param meta Optional metadata to include in the request ({@code _meta} field)
* @return A Mono that emits the page of skills.
*/
public Mono<ListSkillsResult> listSkills(String cursor, Map<String, Object> meta) {
return this.listSkillsInternal(cursor, meta);
}

private Mono<ListSkillsResult> listSkillsInternal(String cursor, Map<String, Object> meta) {
return this.initializer.withInitialization("listing skills",
init -> init.mcpSession()
.sendRequest(McpSchema.METHOD_SKILLS_LIST, new PaginatedRequest(cursor, meta),
LIST_SKILLS_RESULT_TYPE_REF));
}

/**
* Retrieves the current entry for a skill URI, including its manifest.
* @param uri The {@code SKILL.md} URI of the skill
* @return A Mono that emits the skill entry.
*/
public Mono<GetSkillResult> getSkill(String uri) {
return this.getSkill(new GetSkillRequest(uri));
}

/**
* Retrieves the current entry for a skill URI, including its manifest.
* @param getSkillRequest The request containing the {@code SKILL.md} URI
* @return A Mono that emits the skill entry.
*/
public Mono<GetSkillResult> getSkill(GetSkillRequest getSkillRequest) {
return this.initializer.withInitialization("getting skills", init -> init.mcpSession()
.sendRequest(McpSchema.METHOD_SKILLS_GET, getSkillRequest, GET_SKILL_RESULT_TYPE_REF));
}

// --------------------------
// Logging
// --------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
import io.modelcontextprotocol.spec.McpSchema.GetSkillRequest;
import io.modelcontextprotocol.spec.McpSchema.GetSkillResult;
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
import io.modelcontextprotocol.spec.McpSchema.ListSkillsResult;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Mono;

Expand Down Expand Up @@ -326,6 +329,50 @@ public McpSchema.ReadResourceResult readResource(McpSchema.ReadResourceRequest r

}

/**
* Reads a skill resource through the standard {@code resources/read} method.
* @param uri The skill resource URI, including a skill's {@code SKILL.md} URI
* @return The resource content.
*/
public McpSchema.ReadResourceResult readSkillUri(String uri) {
return withProvidedContext(this.delegate.readSkillUri(uri)).block();

}

/**
* Lists every direct child of a directory resource. This method is available only
* when the server's Skills extension declares {@code directoryRead: true}.
* @param uri The directory resource URI
* @return All direct children of the directory.
*/
public McpSchema.ListResourcesResult readDirectory(String uri) {
return withProvidedContext(this.delegate.readDirectory(uri)).block();

}

/**
* Lists one page of direct children of a directory resource.
* @param uri The directory resource URI
* @param cursor Optional pagination cursor from a previous directory read
* @return One page of directory children.
*/
public McpSchema.ListResourcesResult readDirectory(String uri, String cursor) {
return withProvidedContext(this.delegate.readDirectory(uri, cursor)).block();

}

/**
* Lists one page of direct children of a directory resource with optional metadata.
* @param uri The directory resource URI
* @param cursor Optional pagination cursor from a previous directory read
* @param meta Optional metadata to include in the request ({@code _meta} field)
* @return One page of directory children.
*/
public McpSchema.ListResourcesResult readDirectory(String uri, String cursor, Map<String, Object> meta) {
return withProvidedContext(this.delegate.readDirectory(uri, cursor, meta)).block();

}

/**
* Retrieves the list of all resource templates provided by the server.
* @return The list of all resource templates result.
Expand Down Expand Up @@ -423,6 +470,55 @@ public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) {
return withProvidedContext(this.delegate.getPrompt(getPromptRequest)).block();
}

// --------------------------
// Skills Extension
// --------------------------

/**
* Retrieves every skill exposed by a server supporting the Skills extension.
* @return The complete list of skills.
*/
public ListSkillsResult listSkills() {
return withProvidedContext(this.delegate.listSkills()).block();
}

/**
* Retrieves one page of skills exposed by a server supporting the Skills extension.
* @param cursor Optional pagination cursor from a previous list request
* @return The page of skills.
*/
public ListSkillsResult listSkills(String cursor) {
return withProvidedContext(this.delegate.listSkills(cursor)).block();
}

/**
* Retrieves one page of skills, including optional request metadata.
* @param cursor Optional pagination cursor from a previous list request
* @param meta Optional metadata to include in the request ({@code _meta} field)
* @return The page of skills.
*/
public ListSkillsResult listSkills(String cursor, Map<String, Object> meta) {
return withProvidedContext(this.delegate.listSkills(cursor, meta)).block();
}

/**
* Retrieves the current entry for a skill URI, including its manifest.
* @param uri The {@code SKILL.md} URI of the skill
* @return The skill entry.
*/
public GetSkillResult getSkill(String uri) {
return withProvidedContext(this.delegate.getSkill(uri)).block();
}

/**
* Retrieves the current entry for a skill URI, including its manifest.
* @param getSkillRequest The request containing the {@code SKILL.md} URI
* @return The skill entry.
*/
public GetSkillResult getSkill(GetSkillRequest getSkillRequest) {
return withProvidedContext(this.delegate.getSkill(getSkillRequest)).block();
}

/**
* Client can set the minimum logging level it wants to receive from the server.
* @param loggingLevel the min logging level
Expand Down
Loading