sandeepuppalapati/dotnet-mcp-identityserver
If you are the rightful owner of dotnet-mcp-identityserver and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to dayong@mcphub.com.
An ASP.NET Core implementation of the Model Context Protocol (MCP) server with Identity Server JWT authentication, featuring weather tools and Claude AI integration.
.NET MCP Identity Server
A production-ready ASP.NET Core implementation of the Model Context Protocol (MCP) with enterprise-grade JWT authentication, demonstrating how to build secure, context-aware AI applications.
⚠️ Note: This is a demonstration/educational project showcasing MCP integration patterns. Review security settings and test thoroughly before production deployment.
📋 Table of Contents
- The Problem
- The Solution
- Features
- Architecture
- Quick Start
- Configuration
- Usage & Examples
- Technical Decisions
- Development
- Contributing
🎯 The Problem
Modern AI applications need to:
- Authenticate Users: Integrate with enterprise identity systems (OAuth2, SAML, JWT)
- Provide Context: Give AI agents access to user-specific data, APIs, and resources
- Execute Tools: Allow AI to perform actions on behalf of authenticated users
- Maintain Security: Enforce proper authorization and API key management
- Scale Gracefully: Support multiple users with different permission levels
The Model Context Protocol (MCP) standardizes how AI applications access context and tools, but implementing it with enterprise authentication is complex.
Common Challenges
- Authentication Gap: MCP servers often lack production-ready auth mechanisms
- User Context: Tools need access to authenticated user identity and claims
- API Key Management: Different users may have different API keys for external services
- Testing Complexity: Difficult to test MCP flows without proper tooling
💡 The Solution
This project provides a reference implementation demonstrating:
- ✅ Enterprise Authentication: JWT Bearer token validation via Identity Server
- ✅ User-Scoped Tools: MCP tools that use authenticated user context
- ✅ Hierarchical API Keys: User-specific, role-based, and default API key fallbacks
- ✅ Built-in Test Client: Web UI for testing MCP flows without external tools
- ✅ Production Patterns: Proper DI, logging, error handling, and configuration
Real-World Use Cases
- Enterprise AI Assistants: Deploy Claude/GPT with company SSO integration
- Multi-Tenant SaaS: Different customers get different API keys and data access
- Personalized Tools: Weather, calendar, CRM tools that respect user permissions
- Secure Agent Frameworks: Build LangChain/AutoGPT agents with proper auth
✨ Features
🔐 Authentication & Authorization
- JWT Bearer Authentication with configurable Identity Server integration
- Claims-Based Authorization extracting user identity, roles, and company context
- Flexible Token Validation with development-friendly settings
🤖 AI Integration
- Claude AI Integration with tool calling support (Anthropic SDK)
- Multi-Turn Conversations with automatic tool execution
- Demo Mode for testing without API keys
🌤️ Weather Tools (Example Integration)
- Open-Meteo API integration (free, no auth required)
- Location-Based Units: Automatic Celsius/Fahrenheit based on country
- Geocoding: City name to coordinates resolution
- User-Specific API Keys: Premium users can bring their own keys
📡 MCP Protocol Implementation
- Complete MCP Server: Tools, resources, initialization endpoints
- Tool Discovery: Dynamic tool listing with JSON schemas
- Resource Access: User-scoped resources (profile, permissions)
- Protocol Version: MCP 2024-11-05 specification
🌐 Web Test Client
- Interactive UI: Test all MCP endpoints from browser
- Token Management: JWT input and validation
- Response Viewer: Formatted JSON with syntax highlighting
- Tool Testing: Quick access to all implemented tools
🔧 Developer Experience
- Swagger/OpenAPI: Auto-generated API documentation
- Structured Logging: Debug-level logging for services
- Configuration Flexibility: Environment-based settings
- Demo Mode: Run without external dependencies
🏗️ Architecture
System Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Client Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Browser │ │ MCP Client │ │ Mobile App │ │
│ │ Test UI │ │ (Claude │ │ │ │
│ │ │ │ Desktop) │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────┬────────────────┬────────────────┬─────────────────┘
│ │ │
│ JWT Bearer │ JWT Bearer │ JWT Bearer
│ Token │ Token │ Token
│ │ │
┌────────────▼────────────────▼────────────────▼─────────────────┐
│ ASP.NET Core MCP Auth Server │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ JWT Middleware (Program.cs) │ │
│ │ • Validate token signature │ │
│ │ • Extract claims (sub, email, role, etc.) │ │
│ │ • Set HttpContext.User │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼──────────────────────────────┐ │
│ │ Controller Layer │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ MCP │ │ Chat │ │ User │ │ │
│ │ │ Controller │ │ Controller │ │ Controller │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • Initialize │ │ • Claude │ │ • Profile │ │ │
│ │ │ • Tools │ │ Chat │ │ • Claims │ │ │
│ │ │ • Resources │ │ • Tools │ │ • API Keys │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └─────────┬──────────────────┬──────────────────┬─────────┘ │
│ │ │ │ │
│ ┌─────────▼──────────────────▼──────────────────▼─────────┐ │
│ │ Service Layer │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Weather │ │ Claude │ │ UserAPIKey │ │ │
│ │ │ Service │ │ Service │ │ Service │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ • Geocoding │ │ • Messages │ │ • Key │ │ │
│ │ │ • Weather │ │ • Tool Use │ │ Resolution │ │ │
│ │ │ Data │ │ • Response │ │ • Hierarchical│ │ │
│ │ │ • Unit Conv. │ │ Parsing │ │ Fallback │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │
│ └─────────┬──────────────────┬──────────────────┬─────────┘ │
└────────────┼──────────────────┼──────────────────┼───────────┘
│ │ │
┌────────────▼─────┐ ┌─────────▼─────┐ ┌────────▼──────────┐
│ Open-Meteo │ │ Anthropic │ │ Identity Server │
│ Weather API │ │ Claude API │ │ │
│ │ │ │ │ • Token Issuer │
│ • Geocoding │ │ • Messages │ │ • User Store │
│ • Forecast │ │ • Tool Call │ │ • OIDC/OAuth2 │
└──────────────────┘ └───────────────┘ └───────────────────┘
Data Flow: Weather Tool with User Context
1. User → MCP Client: "What's the weather in London?"
2. MCP Client → Server: POST /api/mcp/tools/call
Headers: Authorization: Bearer <JWT>
Body: { "name": "get_weather", "arguments": { "city": "London" } }
3. Server: JWT Middleware validates token, extracts claims
• sub: "user123"
• email: "john@company.com"
• role: "premium"
4. McpController → WeatherService.GetWeatherAsync("London", "user123")
5. WeatherService: Resolve API key
• Check Weather:UserApiKeys:user123 → not found
• Check role "premium" → Weather:RoleApiKeys:Premium → found!
• Use premium API key
6. WeatherService → Open-Meteo:
a. Geocode "London" → {lat: 51.5074, lon: -0.1278, country: "UK"}
b. Get weather → {temp: 18°C, humidity: 72%, clouds: 40%}
7. WeatherService: Determine unit (UK → Celsius)
8. WeatherService → McpController: WeatherData object
9. McpController: Format MCP response
{
"content": [{
"type": "text",
"text": "Hi john! 🌤️ Weather in London, UK\nTemp: 18°C..."
}]
}
10. Server → MCP Client: HTTP 200 with formatted weather
Key Architectural Patterns
1. Authentication Middleware Chain
Request → HTTPS Redirect → Static Files → Authentication → Authorization → Controllers
2. Service Layer Abstraction
All business logic lives in services implementing interfaces:
IWeatherService: Weather data retrievalIClaudeService: AI interactionsIUserApiKeyService: Key management
3. Dependency Injection
// Program.cs
builder.Services.AddHttpClient<IWeatherService, WeatherService>();
builder.Services.AddHttpClient<IClaudeService, ClaudeService>();
builder.Services.AddSingleton<IUserApiKeyService, UserApiKeyService>();
4. Configuration-Driven API Keys
{
"Weather": {
"ApiKey": "default-key", // Fallback
"UserApiKeys": { "user123": "user-key" }, // User-specific
"RoleApiKeys": { "Premium": "premium-key" } // Role-based
}
}
🚀 Quick Start
Prerequisites
- .NET 8.0 SDK or later
- Identity Server instance for JWT issuing (optional for demo mode)
- Claude API Key from Anthropic (optional, demo mode available)
Installation
-
Clone the repository
git clone https://github.com/yourusername/dotnet-mcp-identityserver.git cd dotnet-mcp-identityserver -
Configure settings
# Create development settings (gitignored) cp appsettings.json appsettings.Development.json # Edit with your values nano appsettings.Development.json -
Minimal configuration for demo
{ "IdentityServer": { "Authority": "https://your-identity-server.com", "Audience": "your-client-id", "RequireHttpsMetadata": false // For local development }, "Weather": { "ApiKey": "open-meteo" // Free, no auth required }, "Claude": { "ApiKey": "demo", // Use demo mode "Model": "claude-3-5-sonnet-20241022" } } -
Build and run
dotnet build dotnet run -
Access the application
- Web UI: https://localhost:7000
- Swagger: https://localhost:7000/swagger
First Test (Without Identity Server)
If you don't have an Identity Server, use the test endpoint:
# Get a test token (development only!)
curl https://localhost:7000/api/test/token
# Use the token
curl -H "Authorization: Bearer <token-from-above>" \
https://localhost:7000/api/mcp/tools
⚙️ Configuration
Identity Server Setup
Configure your Identity Server in appsettings.json:
{
"IdentityServer": {
"Authority": "https://your-identity-server.com",
"Audience": "mcp-server-api",
"RequireHttpsMetadata": true
}
}
Required Identity Server Configuration:
- Client ID (Audience):
mcp-server-api - Allowed Scopes:
openid,profile,email - Token Type: JWT (not reference tokens)
Recommended Claims to Include:
sub: User identifier (required)email: User emailnameorfirstName/lastName: Display namerole: User role (for role-based API keys)companyId: Tenant/organization context (optional)
Weather API Configuration
Basic (Free Open-Meteo)
{
"Weather": {
"ApiKey": "open-meteo"
}
}
Advanced (User-Specific Keys)
{
"Weather": {
"ApiKey": "open-meteo", // Default fallback
"PremiumApiKey": "premium-key", // For premium users
"UserApiKeys": {
"user123": "user-specific-key", // Specific user
"admin": "admin-key"
},
"PremiumUsers": {
"user123": true // Mark as premium
},
"RoleApiKeys": {
"Admin": "admin-tier-key", // Role-based
"Premium": "premium-tier-key"
}
}
}
Resolution Order:
UserApiKeys:{userId}- User-specific keyRoleApiKeys:{userRole}- Role-based keyPremiumApiKey(ifPremiumUsers:{userId}is true)ApiKey- Default fallback
Claude AI Configuration
{
"Claude": {
"ApiKey": "sk-ant-api03-...", // Your Anthropic API key
"Model": "claude-3-5-sonnet-20241022"
}
}
Supported Models:
claude-3-5-sonnet-20241022(recommended)claude-3-opus-20240229claude-3-sonnet-20240229claude-3-haiku-20240307
Demo Mode:
Set ApiKey: "demo" to use canned responses without API calls.
Logging Configuration
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"McpAuthServer.Services.ClaudeService": "Debug",
"McpAuthServer.Services.WeatherService": "Debug"
}
}
}
🔧 Usage & Examples
Web Test Client
The built-in web client at https://localhost:7000 provides:
- JWT Token Input: Paste your token from Identity Server
- User Info: View extracted claims from token
- Tool Testing: Quick buttons for all MCP tools
- Resource Browser: View user-scoped resources
- Response Viewer: Formatted JSON output
API Endpoints
MCP Protocol Endpoints
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/mcp/initialize | POST | ✅ | MCP protocol handshake |
/api/mcp/tools | GET | ✅ | List available tools |
/api/mcp/tools/call | POST | ✅ | Execute a tool |
/api/mcp/resources | GET | ✅ | List user resources |
/api/mcp/resources/read | POST | ✅ | Read resource data |
User Management Endpoints
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/user/details | GET | ✅ | User profile and claims |
/api/user/claims | GET | ✅ | All JWT claims |
/api/user/apikeys | GET | ✅ | Configured API keys |
Chat Endpoints
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/chat/completions | POST | ✅ | Simple Claude chat |
/api/chat/tools | POST | ✅ | Claude with tool access |
Example: Weather Tool
Request:
curl -X POST https://localhost:7000/api/mcp/tools/call \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "get_weather",
"arguments": {
"city": "London"
}
}'
Response:
{
"content": [
{
"type": "text",
"text": "Hi John! 🌤️ Here's the weather in London, United Kingdom\nTemperature: 18°C\nCondition: Partly cloudy\nHumidity: 72%\nPressure: 1013 hPa\nWind Speed: 5.5 m/s\nCloud Cover: 40%"
}
]
}
Example: Claude with Tools
Request:
curl -X POST https://localhost:7000/api/mcp/tools/call \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "claude_with_tools",
"arguments": {
"message": "What is the weather like in Paris?"
}
}'
What Happens:
- Request sent to Claude with tool definitions
- Claude responds with
tool_useforget_weather(city: "Paris") - Server executes weather API call
- Results sent back to Claude
- Claude generates natural language response
- Final response returned to client
Response:
{
"content": [
{
"type": "text",
"text": "🧠 Claude with tools:\nThe weather in Paris is currently 22°C with clear skies. It's a beautiful day with 45% humidity and light winds at 3.2 m/s. Perfect weather for a walk along the Seine!"
}
]
}
Example: User Resources
Request:
curl https://localhost:7000/api/mcp/resources \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
Response:
{
"resources": [
{
"uri": "user://profile",
"name": "User Profile",
"description": "Current user profile information"
},
{
"uri": "user://permissions",
"name": "User Permissions",
"description": "Current user permissions and roles"
}
]
}
Read Resource:
curl -X POST https://localhost:7000/api/mcp/resources/read \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "uri": "user://profile" }'
🧠 Technical Decisions
Why ASP.NET Core?
Decision: Use ASP.NET Core instead of Python FastAPI or Node.js Express
Rationale:
- Enterprise Ready: Built-in JWT authentication, DI, configuration
- Performance: Faster than Python/Node for I/O-bound workloads
- Type Safety: C# strong typing prevents runtime errors
- Tooling: Excellent IDE support (Visual Studio, Rider, VS Code)
- .NET 8 Features: Native AOT, improved async, minimal APIs
Trade-offs: Larger initial footprint, fewer MCP examples in C#
Why Identity Server for Auth?
Decision: Use external Identity Server (OAuth2/OIDC) instead of built-in auth
Rationale:
- Enterprise Standard: Most companies already have Identity Server/Azure AD/Okta
- Centralized Auth: Single source of truth for users
- Production Ready: Battle-tested token validation
- Flexibility: Works with any OIDC-compliant provider
Trade-offs: Requires external service, more complex local setup
Why Open-Meteo for Weather?
Decision: Use Open-Meteo API instead of OpenWeatherMap/WeatherAPI
Rationale:
- No API Key Required: Free tier doesn't need registration
- Good Coverage: Global weather data
- No Rate Limits: Generous free usage
- Simple API: Easy to demonstrate concepts
Trade-offs: Less detailed than paid services, no historical data
Why User-Specific API Keys?
Decision: Implement hierarchical API key resolution (user → role → default)
Rationale:
- Multi-Tenancy: Different customers can use their own keys
- Cost Attribution: Track API usage per user/department
- Rate Limits: Avoid hitting global rate limits
- Premium Tiers: Easy to offer paid features
Implementation: WeatherService.GetApiKeyForUser() checks config hierarchy
Why Demo Mode?
Decision: Support "demo" API key for testing without external services
Rationale:
- Developer Experience: Run project immediately after clone
- CI/CD Testing: No secrets needed for tests
- Demonstrations: Show architecture without API setup
Implementation: Services detect ApiKey: "demo" and return canned data
JWT Validation Settings
Decision: Relaxed validation in Program.cs (ValidateIssuerSigningKey=false)
Rationale:
- Development Ease: Some Identity Server setups have certificate issues
- Flexibility: Works with various token issuers
- Clear Warning: Code comments explain this is for development
⚠️ Production: Set ValidateIssuerSigningKey: true and RequireSignedTokens: true
Service Layer Pattern
Decision: Separate controllers from business logic via service interfaces
Rationale:
- Testability: Easy to mock services in tests
- Reusability: Services used by multiple controllers
- Separation of Concerns: HTTP logic vs business logic
- Dependency Injection: Lifetime management (Singleton/Scoped)
Example:
public interface IWeatherService {
Task<WeatherData?> GetWeatherAsync(string city, string? userId);
}
Temperature Unit Localization
Decision: Automatically use Fahrenheit for US, Celsius elsewhere
Rationale:
- User Experience: Show familiar units automatically
- No User Input: Infer from location, not explicit setting
- Simple Logic: Country-based lookup (
GetTemperatureUnit())
Implementation: Open-Meteo geocoding returns country, lookup table maps to unit
Structured Logging
Decision: Use ILogger with structured parameters, debug level for services
Rationale:
- Debugging: Trace API calls and responses
- Production: Info level for high-level operations
- Searchable: Structured logs work with Seq/ELK/Azure Monitor
Example:
_logger.LogInformation("Weather request for {City}, user {UserId}", city, userId);
🧪 Development
Project Structure
dotnet-mcp-identityserver/
├── Controllers/ # API Controllers
│ ├── McpController.cs # MCP protocol (tools, resources)
│ ├── ChatController.cs # Claude chat endpoints
│ ├── UserController.cs # User info, claims, API keys
│ ├── UserApiKeyController.cs # API key management
│ ├── WeatherTestController.cs # Weather testing
│ ├── AuthController.cs # Auth helpers
│ └── TestController.cs # Test token generation
├── Services/ # Business Logic
│ ├── WeatherService.cs # Open-Meteo integration
│ ├── ClaudeService.cs # Anthropic Claude SDK
│ └── UserApiKeyService.cs # API key resolution
├── wwwroot/ # Static Files
│ ├── index.html # Test client UI
│ └── mcp-client.js # Client-side JavaScript
├── Program.cs # App startup & middleware
├── McpAuthServer.csproj # Project file
├── appsettings.json # Configuration (template)
├── appsettings.Development.json # Local settings (gitignored)
├── README.md # This file
├── CLAUDE.md # AI assistant guidance
└── CONTRIBUTING.md # Contribution guidelines
Running Tests
# Run all tests
dotnet test
# Run with detailed output
dotnet test --logger "console;verbosity=detailed"
# Run with code coverage
dotnet test --collect:"XPlat Code Coverage"
# Generate coverage report (requires reportgenerator)
reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coverage
Building for Production
# Optimized release build
dotnet publish -c Release -o ./publish
# Self-contained deployment (includes .NET runtime)
dotnet publish -c Release -r linux-x64 --self-contained
# Single-file executable
dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true
Docker Deployment
Dockerfile:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["McpAuthServer.csproj", "."]
RUN dotnet restore
COPY . .
RUN dotnet build -c Release -o /app/build
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY /app/publish .
ENTRYPOINT ["dotnet", "McpAuthServer.dll"]
Build and run:
# Build image
docker build -t mcp-auth-server:latest .
# Run container
docker run -d -p 8080:80 -p 8081:443 \
-e IdentityServer__Authority=https://your-ids.com \
-e IdentityServer__Audience=mcp-api \
-e Claude__ApiKey=$CLAUDE_API_KEY \
--name mcp-server \
mcp-auth-server:latest
# View logs
docker logs -f mcp-server
Environment Variables
Override configuration via environment variables (Docker/Kubernetes):
# Format: Section__Property
export IdentityServer__Authority="https://ids.company.com"
export IdentityServer__Audience="mcp-api"
export IdentityServer__RequireHttpsMetadata="true"
export Claude__ApiKey="sk-ant-..."
export Weather__ApiKey="open-meteo"
Adding a New Tool
-
Define tool in
McpController.GetTools()new { name = "get_stock_price", description = "Get current stock price", inputSchema = new { type = "object", properties = new { symbol = new { type = "string", description = "Stock ticker" } }, required = new[] { "symbol" } } } -
Add case to
McpController.CallTool()"get_stock_price" => await HandleStockPriceTool(request.Arguments) -
Implement handler
private async Task<IActionResult> HandleStockPriceTool(object? arguments) { var symbol = ExtractStringFromArguments(arguments, "symbol"); var price = await _stockService.GetPriceAsync(symbol); return Ok(new { content = new[] { new { type = "text", text = $"${symbol}: ${price}" } } }); } -
Create service (optional)
public interface IStockService { Task<decimal> GetPriceAsync(string symbol); } -
Register service in
Program.csbuilder.Services.AddHttpClient<IStockService, StockService>();
Debugging Tips
Enable detailed logging:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Debug"
}
}
}
View JWT claims:
curl https://localhost:7000/api/user/claims \
-H "Authorization: Bearer YOUR_TOKEN"
Test without auth:
Use [AllowAnonymous] attribute on controller during development:
[AllowAnonymous] // Remove before production!
public class TestController : ControllerBase { }
View Swagger docs: Navigate to https://localhost:7000/swagger
🤝 Contributing
We welcome contributions! See for guidelines.
Quick Contribution Guide
- Fork the repository
- Create a branch:
git checkout -b feature/amazing-tool - Make changes with tests and documentation
- Commit:
git commit -m "feat: add stock price tool" - Push:
git push origin feature/amazing-tool - Open PR with description of changes
Code Style
- Follow standard C# conventions (PascalCase for public members)
- Add XML documentation to public APIs
- Use dependency injection for all services
- Write unit tests for business logic
- Keep controllers thin (business logic in services)
📄 License
This project is licensed under the MIT License - see the file for details.
🙏 Acknowledgments
- Anthropic - Model Context Protocol specification and Claude AI
- Open-Meteo - Free weather API without auth requirements
- Identity Server - Enterprise authentication standard
- ASP.NET Core Team - Excellent framework
📚 Additional Resources
- MCP Specification - Protocol details
- Claude API Docs - Anthropic API reference
- Identity Server Docs - OIDC/OAuth2 setup
- ASP.NET Core Docs - Framework documentation
- Open-Meteo API - Weather API reference
📞 Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: your-email@example.com
⭐ Star this repository if you find it helpful!
🔗 Share with others building AI applications with enterprise auth!