diff --git a/WORKSHOP.txt b/WORKSHOP.txt index d013c24..cd4fcef 100644 --- a/WORKSHOP.txt +++ b/WORKSHOP.txt @@ -1,7 +1,7 @@ -A coach with a tool -A deterministic practice-guidance function that the agent can call. +InterviewData MCP server +The repository service and emulator run alongside the coach. A supplied probe lists the server's tools. -Checkpoint: 04-tools +Checkpoint: 05-mcp-server Source tag: workshop-v3-reference.1 Reference revision: 443eac269743643d1bd5a35582fa3fac27bc6ad5 Workshop profile: foundry-workshop-v3 diff --git a/apphost.cs b/apphost.cs index beed727..eb5bc5d 100644 --- a/apphost.cs +++ b/apphost.cs @@ -14,6 +14,24 @@ .AddUserSecrets(typeof(Program).Assembly, optional: true, reloadOnChange: true) .Build(); +// Azure Cosmos DB (NoSQL). Uses the local emulator in run mode and provisions a managed +// account when published. Aspire creates the database and container as resources, so no +// runtime resource creation is required (see the EnsureCreatedAsync note in the MCP server). +var cosmos = builder.AddAzureCosmosDB(ResourceConstants.Cosmos); +#pragma warning disable ASPIRECOSMOSDB001 +if (builder.ExecutionContext.IsRunMode) +{ + cosmos.RunAsPreviewEmulator(emulator => emulator.WithDataExplorer()); +} +#pragma warning restore ASPIRECOSMOSDB001 + +var cosmosDb = cosmos.AddCosmosDatabase(ResourceConstants.CosmosDatabase); +cosmosDb.AddContainer(ResourceConstants.CosmosContainer, "/id"); + +var mcpInterviewData = builder.AddProject(ResourceConstants.McpInterviewData) + .WithReference(cosmosDb) + .WaitFor(cosmosDb); + var agent = builder.AddProject(ResourceConstants.Agent) .WithExternalHttpEndpoints() .WithLlmReference(config, args) diff --git a/src/InterviewCoach.Mcp.InterviewData/InterviewSessionTool.cs b/src/InterviewCoach.Mcp.InterviewData/InterviewSessionTool.cs index 2f52064..0a39a5e 100644 --- a/src/InterviewCoach.Mcp.InterviewData/InterviewSessionTool.cs +++ b/src/InterviewCoach.Mcp.InterviewData/InterviewSessionTool.cs @@ -1,6 +1,8 @@ using System.ComponentModel; using ModelContextProtocol; +using ModelContextProtocol.Server; + namespace InterviewCoach.Mcp.InterviewData; public interface IInterviewSessionTool @@ -12,8 +14,10 @@ public interface IInterviewSessionTool Task CompleteInterviewSessionAsync(Guid id); } +[McpServerToolType] public class InterviewSessionTool(IInterviewSessionRepository repository, ILogger logger) : IInterviewSessionTool { + [McpServerTool(Name = "add_interview_session", Title = "Add an interview session")] [Description("Creates a new interview session in the database. Use this after get_interview_session returns no record; update_interview_session cannot create one.")] public async Task AddInterviewSessionAsync( [Description("The interview session details")] InterviewSession record @@ -26,6 +30,7 @@ public async Task AddInterviewSessionAsync( return result; } + [McpServerTool(Name = "get_interview_sessions", Title = "Get a list of interview sessions")] [Description("Gets a list of interview sessions from database.")] public async Task> GetAllInterviewSessionsAsync() { @@ -36,6 +41,7 @@ public async Task> GetAllInterviewSessionsAsync() return interviewSessions; } + [McpServerTool(Name = "get_interview_session", Title = "Get an interview session")] [Description("Gets an interview session from the database by ID. Returns no record when the session has not been created.")] public async Task GetInterviewSessionAsync( [Description("The ID of the interview session")] Guid id @@ -54,6 +60,7 @@ public async Task> GetAllInterviewSessionsAsync() return record; } + [McpServerTool(Name = "update_interview_session", Title = "Update an interview session")] [Description("Updates an existing session: replaces the six document fields and APPENDS Transcript. Fetch first, preserve ResumeLink, ResumeText, ProceedWithoutResume, JobDescriptionLink, JobDescriptionText and ProceedWithoutJobDescription, and send ONLY NEW transcript text. Never resend the stored transcript. This cannot create a record or set IsCompleted; use add_interview_session or complete_interview_session for those operations.")] public async Task UpdateInterviewSessionAsync( [Description("The interview session details")] InterviewSession record @@ -73,6 +80,7 @@ public async Task UpdateInterviewSessionAsync( return updated; } + [McpServerTool(Name = "complete_interview_session", Title = "Complete an interview session")] [Description("Marks an existing interview session as complete and returns the saved record with IsCompleted true. Call this after saving the summary; update_interview_session cannot mark completion. This does not create a missing session.")] public async Task CompleteInterviewSessionAsync( [Description("The exact application-provided session ID to complete")] Guid id diff --git a/src/InterviewCoach.Mcp.InterviewData/Program.cs b/src/InterviewCoach.Mcp.InterviewData/Program.cs index 4361203..90f551d 100644 --- a/src/InterviewCoach.Mcp.InterviewData/Program.cs +++ b/src/InterviewCoach.Mcp.InterviewData/Program.cs @@ -1,3 +1,5 @@ +using System.Reflection; + using InterviewCoach.Mcp.InterviewData; var builder = WebApplication.CreateBuilder(args); @@ -7,6 +9,10 @@ builder.AddCosmosDbContext("interviewdb", "interviewdb"); builder.Services.AddScoped(); +builder.Services.AddMcpServer() + .WithHttpTransport(o => o.Stateless = true) + .WithToolsFromAssembly(Assembly.GetEntryAssembly()); + var app = builder.Build(); app.MapDefaultEndpoints(); @@ -26,4 +32,6 @@ app.UseHttpsRedirection(); } +app.MapMcp("/mcp"); + await app.RunAsync();