dotnet-mcp-identityserver

sandeepuppalapati/dotnet-mcp-identityserver

3.2

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 henry@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.

Tools
2
Resources
0
Prompts
0

.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

Modern AI applications need to:

  1. Authenticate Users: Integrate with enterprise identity systems (OAuth2, SAML, JWT)
  2. Provide Context: Give AI agents access to user-specific data, APIs, and resources
  3. Execute Tools: Allow AI to perform actions on behalf of authenticated users
  4. Maintain Security: Enforce proper authorization and API key management
  5. 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

  1. Enterprise AI Assistants: Deploy Claude/GPT with company SSO integration
  2. Multi-Tenant SaaS: Different customers get different API keys and data access
  3. Personalized Tools: Weather, calendar, CRM tools that respect user permissions
  4. 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 retrieval
  • IClaudeService: AI interactions
  • IUserApiKeyService: 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

  1. Clone the repository

    git clone https://github.com/yourusername/dotnet-mcp-identityserver.git
    cd dotnet-mcp-identityserver
    
  2. Configure settings

    # Create development settings (gitignored)
    cp appsettings.json appsettings.Development.json
    
    # Edit with your values
    nano appsettings.Development.json
    
  3. 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"
      }
    }
    
  4. Build and run

    dotnet build
    dotnet run
    
  5. Access the application

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 email
  • name or firstName/lastName: Display name
  • role: 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:

  1. UserApiKeys:{userId} - User-specific key
  2. RoleApiKeys:{userRole} - Role-based key
  3. PremiumApiKey (if PremiumUsers:{userId} is true)
  4. 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-20240229
  • claude-3-sonnet-20240229
  • claude-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:

  1. JWT Token Input: Paste your token from Identity Server
  2. User Info: View extracted claims from token
  3. Tool Testing: Quick buttons for all MCP tools
  4. Resource Browser: View user-scoped resources
  5. Response Viewer: Formatted JSON output

API Endpoints

MCP Protocol Endpoints
EndpointMethodAuthDescription
/api/mcp/initializePOSTβœ…MCP protocol handshake
/api/mcp/toolsGETβœ…List available tools
/api/mcp/tools/callPOSTβœ…Execute a tool
/api/mcp/resourcesGETβœ…List user resources
/api/mcp/resources/readPOSTβœ…Read resource data
User Management Endpoints
EndpointMethodAuthDescription
/api/user/detailsGETβœ…User profile and claims
/api/user/claimsGETβœ…All JWT claims
/api/user/apikeysGETβœ…Configured API keys
Chat Endpoints
EndpointMethodAuthDescription
/api/chat/completionsPOSTβœ…Simple Claude chat
/api/chat/toolsPOSTβœ…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:

  1. Request sent to Claude with tool definitions
  2. Claude responds with tool_use for get_weather(city: "Paris")
  3. Server executes weather API call
  4. Results sent back to Claude
  5. Claude generates natural language response
  6. 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 --from=publish /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

  1. 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" }
        }
    }
    
  2. Add case to McpController.CallTool()

    "get_stock_price" => await HandleStockPriceTool(request.Arguments)
    
  3. 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}" }
            }
        });
    }
    
  4. Create service (optional)

    public interface IStockService {
        Task<decimal> GetPriceAsync(string symbol);
    }
    
  5. Register service in Program.cs

    builder.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

  1. Fork the repository
  2. Create a branch: git checkout -b feature/amazing-tool
  3. Make changes with tests and documentation
  4. Commit: git commit -m "feat: add stock price tool"
  5. Push: git push origin feature/amazing-tool
  6. 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


πŸ“š Additional Resources


πŸ“ž Support


⭐ Star this repository if you find it helpful!

πŸ”— Share with others building AI applications with enterprise auth!