Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .agents/skills/expose-a-use-case/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The SDK finds every class in the assembly (`WithToolsFromAssembly`, `WithResourc
## Steps

1. Add the use case first (`add-a-use-case`).
2. Pick the kind: a tool when the model should decide to call it, a resource when the client reads a known thing by id, a prompt when a person starts a task.
2. Pick the kind: a tool when the model should decide to call it, a resource when the client reads a known thing by id or a fixed list such as `movies://top-rated`, a prompt when a person starts a task.
3. Choose the name, title and annotations for a tool (`api-conventions`).
4. Write the method with `sender` and `cancellationToken` first, then required inputs, then optional ones with defaults.
5. Document it: `<summary>` saying what it does, what it returns and which tool to call first; a `<param>` for every input except `sender` and the token, with allowed values and where to find ids. Put the XML comment above the attributes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ so pass nextOffset back as the offset to continue rather than raising the limit.
reviews_write creates a review, or changes the stars of one when you pass its reviewId. A review's movie and author can't change.
reviews_delete can't be undone, so confirm with the user before calling it.
Each movie, author and review can also be read as a resource at movies://{id}, authors://{id} or
reviews://{id}, and server://version gives this server's version.
reviews://{id}. movies://top-rated lists the ten best rated movies, and server://version gives this
server's version.
""";
})
.WithHttpTransport(options => options.Stateless = true)
Expand Down
38 changes: 38 additions & 0 deletions src/Presentation/Resources/MovieResources.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace CleanModelContextProtocol.Presentation.Resources;

using Application.Common.Paging;
using Application.Movies.Entities;
using Mediator;
using ModelContextProtocol.Server;
using Presentation.Models;
Expand All @@ -10,6 +12,42 @@ namespace CleanModelContextProtocol.Presentation.Resources;
[McpServerResourceType]
public static partial class MovieResources
{
private const int TopRatedCount = 10;

/// <summary>The ten best rated movies, highest average stars first, with each one's review count.</summary>
[McpServerResource(UriTemplate = "movies://top-rated", Name = "top_rated_movies", Title = "Top rated movies", MimeType = "application/json", IconSource = ItemIcons.Movie)]
public static async partial Task<string> GetTopRatedMovies(
ISender sender,
CancellationToken cancellationToken)
{
var movies = new List<MovieSummary>();
Page<MovieSummary> page;

// The query sorts by title, so ranking needs every movie.
do
{
page = await sender.Send(new Queries.GetMovies.GetMoviesQuery
{
Offset = movies.Count,
Limit = Paging.MaxLimit
}, cancellationToken);

movies.AddRange(page.Items);
}
while (movies.Count < page.TotalCount);

var topRated = movies
.Where(m => m.ReviewCount > 0)
.OrderByDescending(m => m.AverageStars)
.ThenByDescending(m => m.ReviewCount)
.ThenBy(m => m.Title, StringComparer.Ordinal)
.Take(TopRatedCount)
.Select(m => m.ToResult())
.ToList();

return McpSerializer.Serialize(new ResultPage<MovieResult>(topRated, topRated.Count, null));
}

/// <summary>A movie with its review count, average stars and every review, including each author's name.</summary>
/// <param name="movieId">The movie's id (a GUID).</param>
[McpServerResource(UriTemplate = "movies://{movieId}", Name = "movie", Title = "Movie", MimeType = "application/json", IconSource = ItemIcons.Movie)]
Expand Down
20 changes: 16 additions & 4 deletions tests/Presentation.Tests.Integration/Resources/ResourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,27 @@ public async Task ListResourceTemplates_ShouldReturn_EveryTemplate()
}

[Fact]
public async Task ListResources_ShouldReturn_ServerVersion()
public async Task ListResources_ShouldReturn_EveryResource()
{
// Act
var resources = await this.Client.ListResourcesAsync();

// Assert
var resource = resources.ShouldHaveSingleItem();
resource.Uri.ShouldBe("server://version");
resource.MimeType.ShouldBe("application/json");
resources.Select(r => r.Uri).ShouldBe(["server://version", "movies://top-rated"], ignoreOrder: true);
resources.ShouldAllBe(r => r.MimeType == "application/json");
}

[Fact]
public async Task ReadTopRated_ShouldReturn_BestMoviesFirst()
{
// Act
var page = await this.ReadAsync<ResultPage<MovieResult>>("movies://top-rated");

// Assert
page.Items.Count.ShouldBe(10);
page.NextOffset.ShouldBeNull();
page.Items.ShouldAllBe(m => m.ReviewCount > 0);
page.Items.Select(m => m.AverageStars).ShouldBeInOrder(SortDirection.Descending);
}

[Fact]
Expand Down
Loading