Skip to content
Draft
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
14 changes: 7 additions & 7 deletions FabrikamApi/src/FabrikamApi.csproj
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
ο»Ώ<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>df808c62-12d3-45ff-8cce-1f4fc1828c5f</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
</ItemGroup>
Expand Down
9 changes: 5 additions & 4 deletions FabrikamApi/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@
options.AddPolicy("CanViewReports", policy => policy.RequireClaim("permission", "view-reports"));
});

// Configure OpenAPI/Swagger
builder.Services.AddOpenApi();
// Configure Swagger/OpenAPI for .NET 8
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Add CORS for development
builder.Services.AddCors(options =>
Expand All @@ -193,10 +194,10 @@
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/openapi/v1.json", "Fabrikam API v1");
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Fabrikam API v1");
c.RoutePrefix = "swagger"; // Serve Swagger UI at /swagger
c.DocumentTitle = "Fabrikam API Documentation";
c.DefaultModelsExpandDepth(-1); // Hide models section by default
Expand Down
2 changes: 1 addition & 1 deletion FabrikamContracts/FabrikamContracts.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ο»Ώ<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions FabrikamMcp/src/FabrikamMcp.csproj
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
ο»Ώ<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>FabrikamMcp</AssemblyName>
<RootNamespace>FabrikamMcp</RootNamespace>
<UserSecretsId>2823b50c-40fb-478f-b00a-9897a669dfb9</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
<PackageReference Include="ModelContextProtocol" Version="0.2.0-preview.3" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.2.0-preview.3" />
</ItemGroup>
Expand Down
260 changes: 240 additions & 20 deletions FabrikamMcp/src/Program.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
using FabrikamMcp.Tools;
using FabrikamMcp.Services;
using ModelContextProtocol;
using ModelContextProtocol.Server;
using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);

// Add HttpClient for API calls
// Add HttpClient for API calls with extended timeout for long-lived sessions
builder.Services.AddHttpClient();

// Configure ASP.NET Core session services for improved session management
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(60); // Extend session timeout to 60 minutes
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
options.Cookie.SameSite = SameSiteMode.None; // Allow cross-site usage for Copilot Studio
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});

// Add MCP server services with HTTP transport and Fabrikam business tools
builder.Services.AddMcpServer()
.WithHttpTransport()
Expand All @@ -16,17 +28,38 @@
.WithTools<FabrikamProductTools>()
.WithTools<FabrikamBusinessIntelligenceTools>();

// Add CORS for HTTP transport support in browsers
// Add CORS for HTTP transport support in browsers with credentials support
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.AllowAnyOrigin()
policy.SetIsOriginAllowed(_ => true) // Allow any origin for development
.AllowAnyHeader()
.AllowAnyMethod();
.AllowAnyMethod()
.AllowCredentials(); // Enable credentials for session cookies
});
});

// Add memory cache for session management
builder.Services.AddMemoryCache();

// Add distributed memory cache as session store
builder.Services.AddDistributedMemoryCache();

// Register MCP Session Manager as singleton
builder.Services.AddSingleton<IMcpSessionManager, McpSessionManager>();

// Register MCP Error Handler for better session error messages
builder.Services.AddScoped<IMcpErrorHandler, McpErrorHandler>();

// Configure Kestrel for long-lived connections and better session handling
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(10); // Extended keep-alive
options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(5); // Extended request timeout
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB max request size
});

var app = builder.Build();

// Configure the HTTP request pipeline
Expand All @@ -38,27 +71,214 @@
// Enable CORS
app.UseCors();

// Enable session middleware for session management
app.UseSession();

// Add request logging for session debugging with MCP session tracking
app.Use(async (context, next) =>
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
var sessionManager = context.RequestServices.GetRequiredService<IMcpSessionManager>();
var sessionId = context.Session.Id;
var timestamp = DateTime.UtcNow;

logger.LogInformation("MCP Request: {Method} {Path} | Session: {SessionId} | Time: {Timestamp}",
context.Request.Method, context.Request.Path, sessionId, timestamp);

// Track MCP session activity
if (context.Request.Path.StartsWithSegments("/mcp"))
{
var userAgent = context.Request.Headers.UserAgent.ToString();
sessionManager.RegisterSession(sessionId, userAgent);
}
else
{
sessionManager.UpdateSessionActivity(sessionId);
}

await next();

logger.LogInformation("MCP Response: {StatusCode} | Session: {SessionId} | Duration: {Duration}ms",
context.Response.StatusCode, sessionId, (DateTime.UtcNow - timestamp).TotalMilliseconds);
});

// Add MCP session validation middleware
app.Use(async (context, next) =>
{
// Only apply to MCP endpoints
if (context.Request.Path.StartsWithSegments("/mcp") && context.Request.Method == "POST")
{
var sessionManager = context.RequestServices.GetRequiredService<IMcpSessionManager>();
var errorHandler = context.RequestServices.GetRequiredService<IMcpErrorHandler>();
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
var sessionId = context.Session.Id;

// Check if this is an initialize request (allowed for new sessions)
context.Request.EnableBuffering();
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
context.Request.Body.Position = 0;

bool isInitializeRequest = body.Contains("\"method\":\"initialize\"");

// Validate session for non-initialize requests
if (!isInitializeRequest && !sessionManager.IsSessionValid(sessionId))
{
var sessionInfo = sessionManager.GetSessionInfo(sessionId);
object errorResponse;

if (sessionInfo != null)
{
// Session exists but expired
errorResponse = errorHandler.CreateSessionExpiredError(sessionId, sessionInfo.LastActivity);
logger.LogWarning("Rejecting request for expired session: {SessionId}", sessionId);
}
else
{
// Session not found
errorResponse = errorHandler.CreateSessionNotFoundError(sessionId);
logger.LogWarning("Rejecting request for unknown session: {SessionId}", sessionId);
}

context.Response.StatusCode = 404;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(JsonSerializer.Serialize(errorResponse));
return;
}
}

await next();
});

// Map MCP endpoints to the standard /mcp path
app.MapMcp("/mcp");

// Add status and info endpoints
app.MapGet("/status", () => new
// Add status and info endpoints with session health monitoring
app.MapGet("/status", (HttpContext context, IMcpSessionManager sessionManager) =>
{
var sessionId = context.Session.Id;
var sessionKeys = new List<string>();

// Try to access session to ensure it's available
try
{
context.Session.SetString("healthcheck", DateTime.UtcNow.ToString());
var healthCheck = context.Session.GetString("healthcheck");
sessionKeys.Add("healthcheck");
}
catch (Exception ex)
{
// Log session access issues
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogWarning("Session access issue: {Error}", ex.Message);
}

var mcpSession = sessionManager.GetSessionInfo(sessionId);
var allSessions = sessionManager.GetAllSessions();

return new
{
Status = "Ready",
Service = "Fabrikam MCP Server",
Version = "1.0.0",
Description = "Model Context Protocol server for Fabrikam Modular Homes business operations",
Transport = "HTTP",
SessionManagement = new
{
SessionId = sessionId,
SessionTimeout = "60 minutes",
KeepAliveTimeout = "10 minutes",
SessionKeys = sessionKeys,
SessionEnabled = context.Session != null,
McpSessionInfo = mcpSession,
TotalActiveSessions = allSessions.Count(kvp => kvp.Value.IsActive)
},
BusinessModules = new[]
{
"Sales - Order management and customer analytics",
"Inventory - Product catalog and stock monitoring",
"Customer Service - Support ticket management and resolution",
"Products - Product catalog, inventory analytics and management",
"Business Intelligence - Executive dashboards and performance alerts"
},
Timestamp = DateTime.UtcNow,
Environment = app.Environment.EnvironmentName
};
});

// Add session health endpoint for debugging
app.MapGet("/session-health", (HttpContext context, IMcpSessionManager sessionManager) =>
{
var sessionId = context.Session.Id;
var sessionData = new Dictionary<string, object>();

try
{
// Set and get test data
var testKey = "last-access";
var testValue = DateTime.UtcNow.ToString("O");
context.Session.SetString(testKey, testValue);

var retrievedValue = context.Session.GetString(testKey);

sessionData["testKey"] = testKey;
sessionData["testValue"] = testValue;
sessionData["retrievedValue"] = retrievedValue;
sessionData["sessionWorking"] = testValue == retrievedValue;

// Get available session keys (this is limited in ASP.NET Core)
sessionData["sessionId"] = sessionId;
sessionData["isAvailable"] = context.Session.IsAvailable;

// Get MCP session info
var mcpSession = sessionManager.GetSessionInfo(sessionId);
sessionData["mcpSessionInfo"] = mcpSession;
sessionData["mcpSessionValid"] = sessionManager.IsSessionValid(sessionId);

}
catch (Exception ex)
{
sessionData["error"] = ex.Message;
sessionData["sessionWorking"] = false;
}

return new
{
Status = "Session Health Check",
Timestamp = DateTime.UtcNow,
SessionData = sessionData,
Configuration = new
{
SessionTimeout = "60 minutes",
KeepAlive = "10 minutes",
CookiePolicy = "SameSite=None, Secure=SameAsRequest"
}
};
});

// Add sessions management endpoint for monitoring
app.MapGet("/sessions", (IMcpSessionManager sessionManager) =>
{
Status = "Ready",
Service = "Fabrikam MCP Server",
Version = "1.0.0",
Description = "Model Context Protocol server for Fabrikam Modular Homes business operations",
Transport = "HTTP",
BusinessModules = new[]
var allSessions = sessionManager.GetAllSessions();
var activeSessions = allSessions.Where(kvp => kvp.Value.IsActive).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

return new
{
"Sales - Order management and customer analytics",
"Inventory - Product catalog and stock monitoring",
"Customer Service - Support ticket management and resolution",
"Products - Product catalog, inventory analytics and management",
"Business Intelligence - Executive dashboards and performance alerts"
},
Timestamp = DateTime.UtcNow,
Environment = app.Environment.EnvironmentName
Status = "Session Management Overview",
Timestamp = DateTime.UtcNow,
Summary = new
{
TotalSessions = allSessions.Count,
ActiveSessions = activeSessions.Count,
InactiveSessions = allSessions.Count - activeSessions.Count
},
ActiveSessions = activeSessions.Take(10), // Limit to 10 for response size
Configuration = new
{
SessionTimeout = "60 minutes",
CleanupInterval = "5 minutes",
KeepAliveTimeout = "10 minutes"
}
};
});

// Redirect root path to status for convenience
Expand Down
Loading