MCP Landscape for Developers

Oleksii Nikiforov

  • Lead Software Engineer at EPAM Systems
  • AI Coach
  • +10 years in software development
  • Open Source and Blogging
  • Someone who builds MCP Servers used in production 🙂

@nikiforovall
GitHub: nikiforovall
LinkedIn: Oleksii Nikiforov
Blog: nikiforovall.blog

Agenda

  • Introduction to MCP
  • MCP Registry: The App Store for Servers
  • Writing Effective MCP Tools: Production Best Practices
  • Building MCP Servers with .NET
  • MCP in Agentic Systems

What is the Model Context Protocol (MCP)?

MCP (Model Context Protocol) is an open-source standard for connecting AI applications to external systems.

alt: center

MCP Servers Features

%%{init: { 'theme': 'dark', 'themeVariables': { 'fontSize': '22px' }, 'flowchart': { 'nodeSpacing': 500, 'rankSpacing': 0 } }}%% mindmap root(MCP Servers) 🔧 Tools Search Flights Create Events Send Emails ℹ️ Resources Retrieve Documents Access Knowledge Bases Read Calendars 📋 Prompts Plan a Vacation Summarize a Meeting Draft an Email

MCP Clients Features

%%{init: { 'theme': 'dark', 'themeVariables': { 'fontSize': '22px' }, 'flowchart': { 'nodeSpacing': 500, 'rankSpacing': 0 } }}%% mindmap root(MCP Clients) 🤖 Sampling Summarize Email Plan a Trip Decompose a Task 🌲 Roots FolderA FolderB 🗣️ Elicitation Provide a flight destination What is the date? Who is a recipient?

MCP Discovery

The Fragmentation Problem

Before the official registry, developers had to check multiple sources:

Problem: No single source of truth, no standardization, no trust model

MCP Registry: The App Store for Servers

"An app store for MCP servers" - Centralized discovery for the MCP ecosystem


  • Discovery: Find available MCP servers across the ecosystem
  • Trust & Validation: GitHub OAuth, DNS verification, domain ownership
  • Namespace Management: Prevents conflicts (e.g., only @user can publish io.github.user/*)
  • Status: 🆕 Preview Release (September 2025)

Why We Need the Registry

❌ Without a registry, developers face:

  • Fragmentation: Servers scattered across repositories
  • No Trust Model: Can't verify server authenticity
  • Naming Conflicts: Multiple servers with same identifiers
  • Poor Discoverability: Hard to find what you need

The Solution

A centralized, secure registry that:

  • ✅ Makes servers easily discoverable
  • ✅ Ensures authenticity and trust
  • ✅ Manages namespaces to prevent conflicts
  • ✅ Accelerates ecosystem growth

Registry Architecture

%%{init: { 'theme': 'dark', 'themeVariables': { 'fontSize': '28px', 'primaryTextColor': '#ffffff' }, 'flowchart': { 'nodeSpacing': 200, 'rankSpacing': 100 } }}%% flowchart TD Developer -->|Publish| Registry MCPClient -->|Discover| Registry MCPClient -->|Connect| MCPServer1 MCPClient -->|Connect| MCPServer2

Tool Catalog: Discovery in Action

Explore available MCP Servers


🔗 teamsparkai.github.io/ToolCatalog


Writing Effective MCP Tools: Best Practices

❗MCP are NOT Web APIs❗

It is bad for many reasons:

  • OpenAPI is not designed for LLMs
  • LLMs struggle with too many tools and parameters
  • MCP tools should be tailed for the specific problems

Why Tool Quality Matters

Agents are only as effective as the tools we give them

  • Traditional software: deterministic contract between systems
  • MCP Tools: non-deterministic contract between systems and agents
  • Agents can hallucinate, misunderstand, or fail to use tools properly
  • Solution: Design tools specifically for agents, not just wrap APIs

1. Choosing the Right Tools

❌ Don't Just Wrap APIs

❌ list_contacts() - returns ALL contacts
❌ list_users() + list_events() + create_event()
❌ read_logs() - returns entire log file

✅ Design for Agent Workflows

✅ search_contacts(query) - targeted search
✅ schedule_event() - finds availability & schedules
✅ search_logs(filter, context) - relevant logs only

Key Principle: Build tools that match how humans would solve the task

2. Tool Consolidation

Instead of multiple discrete tools:

# ❌ Multiple calls required
user = get_customer_by_id(123)
transactions = list_transactions(123)
notes = list_notes(123)

Build comprehensive tools:

# ✅ Single call with rich context
customer_context = get_customer_context(123)
# Returns: user info, recent transactions, notes, support history

Benefits: Reduces token consumption, fewer API calls, better context

3. Namespacing & Organization

Clear Tool Boundaries

# Service-based namespacing
asana_search_projects()
asana_create_task()
jira_search_issues()
jira_update_status()

# Resource-based namespacing  
asana_projects_search()
asana_users_search()
asana_tasks_create()

Why it matters: Prevents tool confusion when agents have access to hundreds of tools

4. Return Meaningful Context

Prioritize Signal Over Noise

// ❌ Technical, low-signal response
{
  "uuid": "a8f5f167-aa82-44d1-9c4e-8c1a2d8b9e7f",
  "mime_type": "application/json", 
  "256px_image_url": "https://cdn.../thumb_256.jpg",
  "created_timestamp": 1640995200
}

// ✅ Human-readable, high-signal response  
{
  "name": "John Smith",
  "image_url": "https://cdn.../profile.jpg",
  "file_type": "JSON Document",
  "created": "2 days ago"
}

💡 Hint: Use natural language identifiers over UUIDs when possible

5. Response Format Flexibility

Adaptive Detail Levels

def search_messages(query: str,response_format: ResponseFormat = ResponseFormat.CONCISE):
    if response_format == ResponseFormat.DETAILED:
        return {
            "content": "Meeting notes...",
            "thread_id": "1234567890",
            "channel_id": "C1234567890",
            "user_id": "U0987654321"
        }
    else:  # CONCISE
        return {
            "content": "Meeting notes..."
        }

Result: 3x token reduction with concise format while preserving functionality

6. Token Efficiency Strategies

Implement Smart Limits

  • Pagination: Default page sizes (e.g., 10-20 items)
  • Filtering: Built-in search parameters
  • Truncation: Sensible token limits (e.g., 25k tokens)
  • Range Selection: Date ranges, result limits

Helpful Truncation Messages

⚠ Response truncated after 500 results. 
Use filters or pagination to get more specific results:
- Add date_range parameter
- Use more specific search terms
- Request smaller page_size

7. Better Error Handling

❌ Unhelpful Errors

Error: Invalid input
Status: 400
Traceback: ValidationError at line 47...

✅ Actionable Error Messages

Error: Missing required parameter 'user_id'

Expected format: 
- user_id: string (e.g., "john.smith" or "U1234567")

Example: search_user(user_id="john.smith", include_profile=true)

Guide agents toward correct usage patterns

8. Tool Description Best Practices

Write for New Team Members

# ❌ Vague description
def search(query: str, user: str):
    """Search stuff for user"""

# ✅ Clear, detailed description  
def search_slack_messages(
    query: str,           # Search terms (e.g., "project alpha", "meeting notes")
    user_id: str,         # Slack user ID (format: U1234567) or username
    channel_id: str = "", # Optional: limit to specific channel
    date_range: str = "7d" # Search period: "1d", "7d", "30d", or "all"
):
    """
    Search Slack messages and threads for specific content.
    
    Best for: Finding past conversations, meeting notes, decisions
    Returns: Message content, author, timestamp, thread context
    """

9. Collaborative Development with AI

Use Claude Code for Tool Development

  1. Prototype Generation: Let Claude create initial tool implementations
  2. Evaluation Creation: Generate realistic test scenarios
  3. Performance Analysis: Claude analyzes evaluation transcripts
  4. Automatic Optimization: AI refactors tools based on results

Real Results from Anthropic

  • Human-written tools vs Claude-optimized tools
  • Significant performance improvements on held-out test sets
  • Works even better than "expert" manual implementations

The Tool Development Process

%%{init: { 'theme': 'dark', 'themeVariables': { 'fontSize': '32px', 'primaryTextColor': '#ffffff' }, 'flowchart': { 'nodeSpacing': 500, 'rankSpacing': 0 } }}%% flowchart LR A[Build POC 🤔] --> B[Create Evaluations ➕] B --> C[Run Tests 🧪] C --> D[Analyze Results 🔍] D --> E[Pair with AI 🤖] E --> F[Optimize Tools ✨] F --> C F --> G[Production 🚀]

Claude-optimized MCP servers achieve 80.1% accuracy vs 67.4% human-written

10. Evaluation-Driven Development

Create Realistic Test Cases

✅ Strong Evaluation Tasks:

• "Schedule a meeting with Jane next week to discuss our latest 
   Acme Corp project. Attach notes from our last planning meeting 
   and reserve a conference room."

• "Customer ID 9182 reported triple-charging. Find all relevant 
   log entries and check if other customers were affected."

❌ Weak Evaluation Tasks:

• "Schedule a meeting with jane@acme.corp next week"
• "Search logs for customer_id=9182"

11. Metrics That Matter

Track Key Performance Indicators

  • Task Success Rate: % of completed evaluation tasks
  • Token Efficiency: Tokens per successful task
  • Tool Call Patterns: Identify optimization opportunities
  • Error Rates: Parameter validation failures
  • Runtime Performance: Individual tool response times

Use AI for Analysis

Let Claude analyze evaluation transcripts and suggest improvements

12. Production Security Considerations

Input Validation & Sanitization

def execute_query(query: str):
    # ❌ Direct execution
    return database.execute(query)
    
    # ✅ Validated execution
    if not is_safe_query(query):
        raise ValidationError("Query contains unsafe operations")
    return database.execute_safe(query)

Rate Limiting & Resource Management

  • Implement per-tool rate limits
  • Set maximum response sizes
  • Timeout protection for long-running operations

13. Authentication & Authorization

Tool-Level Permissions

@requires_permission("read:customer_data")
def get_customer_context(customer_id: str):
    """Retrieves customer information"""
    
@requires_permission("write:customer_data")  
def update_customer_status(customer_id: str, status: str):
    """Updates customer status"""

MCP Annotations

Use MCP tool annotations to declare:

  • Tools that make destructive changes
  • Required permission levels

Key Takeaways: Production-Ready MCP Tools

  1. 🎯 Purpose-Built: Design for agents, not just API wrappers
  2. 🔄 Iterative: Use evaluation-driven development process
  3. 🎛️ Consolidation: Fewer, more powerful tools beat many simple ones
  4. 💬 Context-Aware: Return meaningful, human-readable responses
  5. ⚡ Token-Efficient: Implement pagination, filtering, truncation
  6. 🛡️ Error-Friendly: Provide actionable error messages
  7. 📝 Well-Documented: Write clear descriptions with examples
  8. 🔒 Secure: Validate inputs, implement proper auth
  9. 📊 Measurable: Track performance with realistic evaluations
  10. 🤖 AI-Assisted: Use AI to optimize your tools

Building MCP Servers with .NET

Simple Installation

dotnet new install Nall.ModelContextProtocol.Template::0.9.0

# Template Name         Short Name            Language  Tags
# --------------------  --------------------  --------  -------------
# MCP Server            mcp-server            [C#]      dotnet/ai/mcp
# MCP Server HTTP       mcp-server-http       [C#]      dotnet/ai/mcp
# MCP Server HTTP Auth  mcp-server-http-auth  [C#]      dotnet/ai/mcp
# MCP Server Hybrid     mcp-server-hybrid     [C#]      dotnet/ai/mcp

And Getting Started...

dotnet new mcp-server -n MyFirstMcp -o MyFirstMCP --dry-run

# File actions would have been taken:
#   Create: MyFirstMCP\MyFirstMcp.csproj
#   Create: MyFirstMCP\Program.cs
#   Create: MyFirstMCP\Properties\launchSettings.json
#   Create: MyFirstMCP\README.md
#   Create: MyFirstMCP\appsettings.Development.json
#   Create: MyFirstMCP\appsettings.json

Echo Server: Program.cs

var builder = Host.CreateApplicationBuilder(args);

builder.Logging.AddConsole(cfg =>
{
    cfg.LogToStandardErrorThreshold = LogLevel.Trace;
});

builder.Services
  .AddMcpServer()
  .WithStdioServerTransport()
  .WithToolsFromAssembly();

builder.Services.AddTransient<EchoTool>();

await builder.Build().RunAsync();

Echo Server: EchoTool.cs

[McpServerToolType]
public class EchoTool(ILogger<EchoTool> logger)
{
    [
        McpServerTool(
            Destructive = false,
            Idempotent = true,
            Name = "echo_hello",
            OpenWorld = false,
            ReadOnly = true,
            Title = "Write a hello message to the client",
            UseStructuredContent = false
        ),
        Description("Echoes the message back to the client.")
    ]
    [return: Description("The echoed message")]
    public string Echo([Description("The message to echo back")] string message)
    {
        logger.LogInformation("Echo called with message: {Message}", message);

        return $"hello, {message}!";
    }
}

Run It 🚀

npx @modelcontextprotocol/inspector -h

#Usage: inspector-bin [options]

#Options:
#  -e <env>               environment variables in KEY=VALUE format (default: {})
#  --config <path>        config file path
#  --server <n>           server name from config file
#  --cli                  enable CLI mode
#  --transport <type>     transport type (stdio, sse, http)
#  --server-url <url>     server URL for SSE/HTTP transport
#  --header <headers...>  HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports) (default: {})
#  -h, --help             display help for command
npx @modelcontextprotocol/inspector dotnet run -v q

How can I distribute my MCPs so people can use them?

🎁 Pack it:

dotnet nuget pack MyFirstMcp.csproj -o ./artifacts

🚢 Ship it:

dotnet nuget push ./artifacts/MyFirstMcp.1.0.0.nupkg

🚀 Run it:

dnx MyFirstMcp

Running tools without installing them with dnx

> dnx --help

# Description:
#   Executes a tool from source without permanently installing it.

# Usage:
#   dotnet dnx <packageId> [<commandArguments>...] [options]

# Arguments:
#   <PACKAGE_ID>        Package reference in the form of a package identifier
#   <commandArguments>  Arguments forwarded to the tool

Learn More: Blog Series

📚 Deep dive into my blog posts about MCP with .NET:

  1. Simplifying Model Context Protocol (MCP) Server Distribution with .NET Global Tools
  2. Simplifying Model Context Protocol (MCP) Server Development with Aspire
  3. Learn how to use Model Context Protocol (MCP) Server Template in Hybrid Mode
  4. Background Job Scheduling Using Hangfire MCP Server
  5. Hangfire MCP Server in Standalone Mode
  6. 🆕 Add Authentication to MCP Servers using Microsoft Entra ID

Securing MCP Servers with OAuth2.1

MCP Security Fundamentals

"When building on top of a fast-paced technology like MCP, it's key that you start with security as a foundation, not an afterthought."

Why Security Matters

  • MCP servers act as bridges between AI agents and data sources
  • Security breaches can:
    • Compromise sensitive data
    • Manipulate AI behavior and decision-making
    • Lead to unauthorized access to connected systems

Key Security Principles

  • 🔒 Defense in Depth: Multiple layers of security controls
  • 🎯 Least Privilege: Minimal access rights for users and systems
  • 📊 Observability: Comprehensive logging and monitoring

OAuth 2.1 Architecture Overview

Authorization Flow

  1. Discovery Phase: Unauthenticated access returns metadata URL
  2. Client Registration: Dynamic client registration with authorization server. Some clients may be pre-registered ...
  3. User Consent: User provides authorization and consent
  4. Token Exchange:MCP client exchanges authorization code for access token.
  5. Authenticated requests. All subsequent requests from MCP client to MCP server include Bearer token.

Key Components

  • Protected Resource Metadata (PRM): OAuth 2.0 specification for resource protection. The MCP server must implement the /.well-known/oauth-protected-resource
  • Resource Indicators: Prevents token reuse across different resources
  • Token Validation: Verify user identity and permissions on every request

Create new Project with Authentication Enabled

dotnet new mcp-server-http-auth -n MyFirstAuthMcp -o MyFirstAuthMcp --dry-run

#  Create: MyFirstAuthMcp\.vscode\mcp.json
#  Create: MyFirstAuthMcp\MyFirstAuthMcp.csproj
#  Create: MyFirstAuthMcp\Program.cs
#  Create: MyFirstAuthMcp\Properties\launchSettings.json
#  Create: MyFirstAuthMcp\README.md
#  Create: MyFirstAuthMcp\Tools\EchoTool.cs
#  Create: MyFirstAuthMcp\Tools\UserService.cs
#  Create: MyFirstAuthMcp\appsettings.Development.json
#  Create: MyFirstAuthMcp\appsettings.json

Auth Server: Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<UserService>();

builder
    .Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
    })
    .AddMcp(options =>
    {
        var identityOptions = builder
            .Configuration.GetSection("AzureAd")
            .Get<MicrosoftIdentityOptions>()!;

        options.ResourceMetadata = new ProtectedResourceMetadata
        {
            Resource = GetMcpServerUrl(),
            AuthorizationServers = [GetAuthorizationServerUrl(identityOptions)],
            ScopesSupported = [$"api://{identityOptions.ClientId}/Mcp.User"],
        };
    })
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddMcpServer().WithToolsFromAssembly().WithHttpTransport();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapMcp().RequireAuthorization();

// Run the web server
app.Run();

Auth Server: EchoTool.cs

[McpServerToolType]
public class EchoTool(UserService userService)
{
    [McpServerTool(
        Name = "Echo",
        Title = "Echoes the message back to the client.",
        UseStructuredContent = true
    )]
    [Description("This tool echoes the message back to the client.")]
    public EchoResponse Echo(string message) =>
        new($"hello {message} from {userService.UserName}", userService.UserName!);
}

public record EchoResponse(string Message, string UserName);

Configure Azure AD App Registration

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "",
    "TenantId": "",
    "ClientId": "",
    "ClientSecret": ""
  },
  "McpServerUrl": "https://localhost:7000"
}

Run It 🚀

Run the server:

dotnet run

Check it from the Inspector:

npx @modelcontextprotocol/inspector --transport http --server-url https://localhost:7000

But there is a better way - use VSCode mcp.json config:

{
  "servers": {
    "echo-mcp": {"url": "https://localhost:7000", "type": "http"}
  }
}

MCP in Agentic Systems

Microsoft Agent Framework and Azure AI Foundry

Microsoft Agent Framework

  • Cross-platform: .NET (C#) and Python support
  • Unified abstraction: Common AIAgent base for all agent types
  • Multi-backend: Azure OpenAI, OpenAI, Azure AI Foundry, Ollama, custom
  • Built-in features: Function calling, multi-turn conversations, structured output, streaming
  • Workflows: Type-safe orchestration of multiple agents and business processes

Azure AI Foundry

  • Unified platform for building, evaluating, and deploying AI agents
  • Server-side persistent agents with built-in MCP support
  • Enterprise-ready: Authentication, monitoring, compliance, scalability

Building Agents with MCP Tools: Code Example

// Create an MCP tool definition for the agent
var mcpTool = new MCPToolDefinition(
    serverLabel: "microsoft_learn",
    serverUrl: "https://learn.microsoft.com/api/mcp"
);
mcpTool.AllowedTools.Add("microsoft_docs_search");

// Create a persistent agent with MCP tools
var agentMetadata = await persistentAgentsClient.Administration.CreateAgentAsync(
    model: "gpt-4o-mini",
    name: "MicrosoftLearnAgent",
    instructions: "You answer questions by searching Microsoft Learn content only.",
    tools: [mcpTool]
);

// Retrieve the agent as an AIAgent
AIAgent agent = await persistentAgentsClient.GetAIAgentAsync(agentMetadata.Value.Id);

Full example: samples/agent_03_foundry_mcp.cs

Using the Agent: Code Example

// Configure MCP tool resources with approval settings
var runOptions = new ChatClientAgentRunOptions()
{
    ChatOptions = new()
    {
        RawRepresentationFactory = (_) => new ThreadAndRunOptions()
        {
            ToolResources = new MCPToolResource(serverLabel: "microsoft_learn")
            {
                RequireApproval = new MCPApproval("never"),
            }.ToToolResources(),
        },
    },
};

// Create a thread and run the agent
AgentThread thread = agent.GetNewThread();
var response = await agent.RunAsync(
    "Please find what's new in .NET 10. Hint: Use the 'microsoft_docs_search' tool.",
    thread,
    runOptions
);

Result: Agent automatically calls the MCP tool to search Microsoft Learn documentation

What We've Learned Today

%%{init: { 'theme': 'dark', 'themeVariables': { 'fontSize': '20px' }, 'flowchart': { 'nodeSpacing': 500, 'rankSpacing': 0 } }}%% mindmap root((MCP Landscape)) 🌐 MCP Fundamentals Protocol Architecture Client-Server Model Tools 📦 MCP Registry Centralized Discovery Trust & Validation ✍️ Writing Effective Tools Design for Agents Tool Consolidation Token Efficiency Error Handling Evaluation-Driven Dev 🔒 Security OAuth 2.1 Authentication 🛠️ Building with .NET MCP Server Template Stdio & HTTP Transport 🤖 Agentic Systems Microsoft Agent Framework Azure AI Foundry MCP Integration

https://modelcontextprotocol.io/docs/learn/server-concepts

show different servers remote vs local (stdio), explain the difference

spend some time and demonstrate examples of good mcp servers: markitdown, browsertools, playwright, context7, microsoft docs

https://github.com/modelcontextprotocol/registry?tab=readme-ov-file

Appratenly people think differently Projects client like https://gofastmcp.com/integrations/openapi tries to do it but I really think it is really bad idea and it is only good for prototyping

https://www.anthropic.com/engineering/writing-tools-for-agents