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
1 change: 1 addition & 0 deletions ModelContextProtocol.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<Project Path="src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" />
<Project Path="src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj" />
<Project Path="src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj" />
<Project Path="src/ModelContextProtocol.Extensions.Skills/ModelContextProtocol.Extensions.Skills.csproj" />
<Project Path="src/ModelContextProtocol.Extensions.Tasks/ModelContextProtocol.Extensions.Tasks.csproj" />
<Project Path="src/ModelContextProtocol/ModelContextProtocol.csproj" />
</Folder>
Expand Down
11 changes: 9 additions & 2 deletions src/ModelContextProtocol.Core/Protocol/PaginatedRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,15 @@ namespace ModelContextProtocol.Protocol;
/// </remarks>
public abstract class PaginatedRequestParams : RequestParams
{
/// <summary>Prevent external derivations.</summary>
private protected PaginatedRequestParams()
/// <summary>
/// Initializes a new instance of the <see cref="PaginatedRequestParams"/> class.
/// </summary>
/// <remarks>
/// This is <see langword="protected"/> rather than <c>private protected</c> so that extension packages
/// implementing paginated methods defined outside the core specification can derive from it. See
/// <see cref="Server.McpServerRequestHandler"/>.
/// </remarks>
protected PaginatedRequestParams()
{
}

Expand Down
10 changes: 9 additions & 1 deletion src/ModelContextProtocol.Core/Protocol/PaginatedResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ namespace ModelContextProtocol.Protocol;
/// </remarks>
public abstract class PaginatedResult : Result
{
private protected PaginatedResult()
/// <summary>
/// Initializes a new instance of the <see cref="PaginatedResult"/> class.
/// </summary>
/// <remarks>
/// This is <see langword="protected"/> rather than <c>private protected</c> so that extension packages
/// implementing paginated methods defined outside the core specification can derive from it. See
/// <see cref="Server.McpServerRequestHandler"/>.
/// </remarks>
protected PaginatedResult()
{
}

Expand Down
20 changes: 20 additions & 0 deletions src/ModelContextProtocol.Extensions.Skills/McpSkillsJsonContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Extensions.Skills;

/// <summary>
/// Provides source-generated JSON serialization metadata for MCP Skills extension types.
/// </summary>
[JsonSourceGenerationOptions(
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(SkillEntry))]
[JsonSerializable(typeof(SkillResource))]
[JsonSerializable(typeof(SkillResources))]
[JsonSerializable(typeof(ListSkillsRequestParams))]
[JsonSerializable(typeof(ListSkillsResult))]
[JsonSerializable(typeof(GetSkillRequestParams))]
[JsonSerializable(typeof(GetSkillResult))]
public sealed partial class McpSkillsJsonContext : JsonSerializerContext
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0;netstandard2.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<PackageId>ModelContextProtocol.Extensions.Skills</PackageId>
<Description>MCP Skills extension (SEP-2640) for the .NET Model Context Protocol (MCP) SDK</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
<!-- Suppress the experimental MCP warnings for internal usage -->
<NoWarn>$(NoWarn);MCPEXP001;MCPEXP002</NoWarn>
<!--
Package validation stays enabled so cross-target-framework API compatibility is checked. The
repo-wide baseline is cleared because this package has no released version to compare against yet;
set it to the first shipped version once this package has been released.
-->
<PackageValidationBaselineVersion />
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<!-- CS0436: Allow ObsoleteAttribute to be redefined internally -->
<NoWarn>$(NoWarn);CS0436</NoWarn>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\Common\Experimentals.cs" Link="Experimentals.cs" />
<Compile Include="..\Common\McpProtocolVersions.cs" Link="McpProtocolVersions.cs" />
</ItemGroup>

<!-- Exclude polyfills inherited from Directory.Build.props; this package only needs the ExperimentalAttribute polyfill. -->
<ItemGroup>
<Compile Remove="..\Common\Polyfills\**\*.cs" />
<Compile Include="..\Common\Polyfills\System\Diagnostics\CodeAnalysis\ExperimentalAttribute.cs"
Link="Polyfills\ExperimentalAttribute.cs" />
<Compile Include="..\Common\Polyfills\System\Runtime\CompilerServices\CompilerFeatureRequiredAttribute.cs"
Link="Polyfills\CompilerFeatureRequiredAttribute.cs" />
<Compile Include="..\Common\Polyfills\System\Runtime\CompilerServices\IsExternalInit.cs"
Link="Polyfills\IsExternalInit.cs" />
<Compile Include="..\Common\Polyfills\System\Runtime\CompilerServices\RequiredMemberAttribute.cs"
Link="Polyfills\RequiredMemberAttribute.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ModelContextProtocol\ModelContextProtocol.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="..\PACKAGE.md" Pack="true" PackagePath="\README.md" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Extensions.Skills;

/// <summary>
/// Represents the parameters for a <c>skills/get</c> request retrieving a single skill's entry by URI.
/// </summary>
/// <remarks>
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640">SEP-2640</see>
/// specification for details.
/// </remarks>
public sealed class GetSkillRequestParams : RequestParams
{
/// <summary>
/// Gets or sets the URI of the skill's <c>SKILL.md</c>.
/// </summary>
/// <remarks>
/// If the URI does not identify a skill the server serves, the server returns error -32602
/// (Invalid params), the same code <c>resources/read</c> uses for unknown resources.
/// </remarks>
[JsonPropertyName("uri")]
public required string Uri { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Extensions.Skills;

/// <summary>
/// Represents a server's response to a <c>skills/get</c> request, containing one skill's entry.
/// </summary>
/// <remarks>
/// <para>
/// A server answers for every skill it serves, whether or not that skill appears in its <c>skills/list</c>
/// result. The result carries no pagination cursor, because a single entry is not a list.
/// </para>
/// <para>
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640">SEP-2640</see>
/// specification for details.
/// </para>
/// </remarks>
public sealed class GetSkillResult : Result
{
/// <summary>
/// Gets or sets the skill's entry, identical in shape and meaning to an entry of <c>skills/list</c>.
/// </summary>
[JsonPropertyName("skill")]
public required SkillEntry Skill { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Extensions.Skills;

/// <summary>
/// Represents the parameters for a <c>skills/list</c> request enumerating the skills a server serves.
/// </summary>
/// <remarks>
/// <para>
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640">SEP-2640</see>
/// specification for details.
/// </para>
/// </remarks>
public sealed class ListSkillsRequestParams : PaginatedRequestParams;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using ModelContextProtocol.Protocol;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Extensions.Skills;

/// <summary>
/// Represents a server's response to a <c>skills/list</c> request, containing the skills it serves.
/// </summary>
/// <remarks>
/// <para>
/// The result may be empty or partial. A server whose skill catalog is large, generated on demand, or
/// otherwise unenumerable may return fewer skills than it serves, and hosts must not treat an empty
/// listing as proof that a server has no skills. Skills absent from a listing remain retrievable through
/// <c>skills/get</c>.
/// </para>
/// <para>
/// An entry is atomic: a skill's manifest is never split across pages.
/// </para>
/// <para>
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640">SEP-2640</see>
/// specification for details.
/// </para>
/// </remarks>
public sealed class ListSkillsResult : PaginatedResult, ICacheableResult
{
/// <summary>
/// Gets or sets the skill entries.
/// </summary>
[JsonPropertyName("skills")]
public IList<SkillEntry> Skills { get; set; } = [];

/// <inheritdoc />
[JsonPropertyName("ttlMs")]
[JsonConverter(typeof(TimeSpanMillisecondsConverter))]
public TimeSpan? TimeToLive { get; set; }

/// <inheritdoc />
/// <remarks>
/// Core applies its own internal <c>CacheScopeConverter</c> to this property on the built-in result
/// types, which tolerates unrecognized scope strings on read by mapping them to
/// <see langword="null"/>. That converter is not accessible outside the core assembly, so this type
/// relies on the converter declared on the <see cref="Protocol.CacheScope"/> enum instead. The written
/// wire values are identical; only the read-side leniency differs.
/// </remarks>
[JsonPropertyName("cacheScope")]
public CacheScope? CacheScope { get; set; }
}
49 changes: 49 additions & 0 deletions src/ModelContextProtocol.Extensions.Skills/Protocol/SkillEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Extensions.Skills;

/// <summary>
/// Represents a single skill as returned by <c>skills/list</c> and <c>skills/get</c>.
/// </summary>
/// <remarks>
/// <para>
/// An entry is a complete, point-in-time snapshot of a skill: its <c>SKILL.md</c> URI, the verbatim
/// frontmatter of that file, and the manifest of the skill's files. A host that pages through a listing
/// therefore has everything it needs to build its registry and verify every file it later reads, without
/// a second round-trip per skill.
/// </para>
/// <para>
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640">SEP-2640</see>
/// specification for details.
/// </para>
/// </remarks>
public sealed class SkillEntry
{
/// <summary>
/// Gets or sets the resource URI of the skill's <c>SKILL.md</c>.
/// </summary>
/// <remarks>
/// The final path segment preceding <c>/SKILL.md</c> must equal the <c>name</c> field of
/// <see cref="Frontmatter"/>, so that a skill's name is recoverable from its URI alone.
/// </remarks>
[JsonPropertyName("uri")]
public required string Uri { get; set; }

/// <summary>
/// Gets or sets the skill's <c>SKILL.md</c> YAML frontmatter rendered verbatim as a JSON object.
/// </summary>
/// <remarks>
/// Every field the author wrote is passed through, not a curated subset. Hosts re-parse the fetched
/// <c>SKILL.md</c> and compare it against this object field by field, treating any discrepancy as a
/// verification failure, so this must reproduce the authored frontmatter exactly.
/// </remarks>
[JsonPropertyName("frontmatter")]
public required JsonObject Frontmatter { get; set; }

/// <summary>
/// Gets or sets the skill's file manifest.
/// </summary>
[JsonPropertyName("resources")]
public required SkillResources Resources { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Extensions.Skills;

/// <summary>
/// Represents a single file belonging to a skill, as listed in a <see cref="SkillEntry.Resources"/> manifest.
/// </summary>
/// <remarks>
/// <para>
/// This is distinct from <see cref="ModelContextProtocol.Protocol.Resource"/>, the base protocol's resource
/// metadata type. A <see cref="SkillResource"/> carries only the integrity information a host needs to
/// verify a skill's file: its URI, digest, and size.
/// </para>
/// <para>
/// See the <see href="https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640">SEP-2640</see>
/// specification for details.
/// </para>
/// </remarks>
public sealed class SkillResource
{
/// <summary>
/// Gets or sets the resource URI of the file.
/// </summary>
[JsonPropertyName("uri")]
public required string Uri { get; set; }

/// <summary>
/// Gets or sets the SHA-256 digest of the file, formatted as <c>sha256:{hex}</c> where <c>{hex}</c>
/// is 64 lowercase hexadecimal characters.
/// </summary>
/// <remarks>
/// Digests are unsigned and supplied by the same server that supplies the content. A match proves the
/// listing and the content are consistent; it is not a security boundary and must not be treated as one.
/// </remarks>
[JsonPropertyName("digest")]
public required string Digest { get; set; }

/// <summary>
/// Gets or sets the length in bytes of the file's raw content, being the same bytes the
/// <see cref="Digest"/> covers.
/// </summary>
/// <remarks>
/// A read whose byte length differs from this value is a verification failure equivalent to a digest
/// mismatch, whether or not the digest is subsequently computed.
/// </remarks>
[JsonPropertyName("size")]
public required long Size { get; set; }
}
Loading