Model Context Protocol (MCP)

An open protocol for connecting AI assistants with external tools, data sources, and services. Created by Anthropic to standardize how LLMs interact with the outside world.

What is MCP?

The Model Context Protocol (MCP) is an open standard that defines how AI applications communicate with external tools and data sources. Think of it as a USB standard for AI — any MCP-compatible client can work with any MCP server.

Tools

Functions the LLM can invoke (search, create, update, delete)

Resources

Data the LLM can read (files, database records, API responses)

Prompts

Pre-defined templates for common tasks

Why MCP Matters

Before MCP, every application needed custom integrations for each service. MCP provides a standard interface, allowing developers to build tools once and use them with any MCP-compatible AI assistant.

Architecture

MCP Architecture
┌─────────────────────────────────────────────────────────────────┐
│                        AI APPLICATION                            │
│  (Claude Desktop, VS Code, Custom Agent)                        │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │                      MCP CLIENT                            │ │
│  │  • Discovers servers from config                           │ │
│  │  • Manages connections                                     │ │
│  │  • Routes tool calls to correct server                     │ │
│  └────────────────────────────────────────────────────────────┘ │
└───────────────┬────────────────┬────────────────┬───────────────┘
                │ stdio          │ stdio          │ stdio
                ▼                ▼                ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│   MCP SERVER      │ │   MCP SERVER      │ │   MCP SERVER      │
│   (GitHub)        │ │   (Slack)         │ │   (Database)      │
│                   │ │                   │ │                   │
│ Tools:            │ │ Tools:            │ │ Tools:            │
│ • search_repos    │ │ • send_message    │ │ • query           │
│ • get_file        │ │ • list_channels   │ │ • insert          │
│ • create_issue    │ │ • search_messages │ │ • update          │
│                   │ │                   │ │                   │
│ Resources:        │ │ Resources:        │ │ Resources:        │
│ • github://repos  │ │ • slack://users   │ │ • db://tables     │
└───────────────────┘ └───────────────────┘ └───────────────────┘
         │                    │                    │
         ▼                    ▼                    ▼
    GitHub API           Slack API            PostgreSQL
MCP Server Implementation
# MCP Architecture Overview

# SERVER: Exposes tools and resources
class MCPServer:
    name: "github-server"
    version: "1.0.0"

    # Tools the server provides
    tools: [
        { name: "search_repos", description: "Search GitHub repositories" },
        { name: "get_file", description: "Get file contents from repo" },
        { name: "create_issue", description: "Create a new issue" }
    ]

    # Resources the server exposes
    resources: [
        { uri: "github://repos/{owner}/{repo}", description: "Repository info" },
        { uri: "github://issues/{owner}/{repo}", description: "Issue list" }
    ]

    # Handle tool invocation
    function handleToolCall(toolName, arguments):
        switch toolName:
            case "search_repos":
                return githubAPI.searchRepos(arguments.query)
            case "get_file":
                return githubAPI.getFile(arguments.repo, arguments.path)
            # ...

# CLIENT: Connects to servers and routes requests
class MCPClient:
    servers: Map<string, MCPServer>

    function connect(serverConfig):
        server = spawn(serverConfig.command, serverConfig.args)
        capabilities = server.initialize()
        servers.set(serverConfig.name, server)
        return capabilities

    function callTool(serverName, toolName, arguments):
        server = servers.get(serverName)
        return server.tools.call(toolName, arguments)
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationOptions
from mcp.types import Tool, TextContent, Resource
import mcp.server.stdio

# Create an MCP server
server = Server("github-server")

# Define available tools
@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="search_repos",
            description="Search GitHub repositories by query",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "Search query"
                    },
                    "limit": {
                        "type": "integer",
                        "default": 10
                    }
                },
                "required": ["query"]
            }
        ),
        Tool(
            name="get_file",
            description="Get file contents from a repository",
            inputSchema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string"},
                    "repo": {"type": "string"},
                    "path": {"type": "string"}
                },
                "required": ["owner", "repo", "path"]
            }
        ),
        Tool(
            name="create_issue",
            description="Create a new issue in a repository",
            inputSchema={
                "type": "object",
                "properties": {
                    "owner": {"type": "string"},
                    "repo": {"type": "string"},
                    "title": {"type": "string"},
                    "body": {"type": "string"}
                },
                "required": ["owner", "repo", "title"]
            }
        )
    ]

# Handle tool calls
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "search_repos":
        results = await github_api.search_repos(arguments["query"])
        return [TextContent(type="text", text=json.dumps(results))]

    elif name == "get_file":
        content = await github_api.get_file(
            arguments["owner"],
            arguments["repo"],
            arguments["path"]
        )
        return [TextContent(type="text", text=content)]

    elif name == "create_issue":
        issue = await github_api.create_issue(
            arguments["owner"],
            arguments["repo"],
            arguments["title"],
            arguments.get("body", "")
        )
        return [TextContent(type="text", text=f"Created issue #{issue.number}")]

    raise ValueError(f"Unknown tool: {name}")

# Define available resources
@server.list_resources()
async def list_resources() -> list[Resource]:
    return [
        Resource(
            uri="github://repos/{owner}/{repo}",
            name="Repository Information",
            description="Get metadata about a GitHub repository",
            mimeType="application/json"
        )
    ]

# Run the server
async def main():
    async with mcp.server.stdio.stdio_server() as (read, write):
        await server.run(
            read,
            write,
            InitializationOptions(
                server_name="github-server",
                server_version="1.0.0"
            )
        )

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())
using Microsoft.Extensions.AI;
using ModelContextProtocol;
using ModelContextProtocol.Server;

// Define an MCP server
public class GitHubMcpServer : McpServer
{
    private readonly IGitHubClient _github;

    public GitHubMcpServer(IGitHubClient github)
    {
        _github = github;
    }

    public override ServerInfo GetServerInfo() => new()
    {
        Name = "github-server",
        Version = "1.0.0"
    };

    public override IEnumerable<Tool> ListTools()
    {
        yield return new Tool
        {
            Name = "search_repos",
            Description = "Search GitHub repositories by query",
            InputSchema = new JsonSchema
            {
                Type = "object",
                Properties = new Dictionary<string, JsonSchema>
                {
                    ["query"] = new() { Type = "string", Description = "Search query" },
                    ["limit"] = new() { Type = "integer", Default = 10 }
                },
                Required = new[] { "query" }
            }
        };

        yield return new Tool
        {
            Name = "get_file",
            Description = "Get file contents from a repository",
            InputSchema = new JsonSchema
            {
                Type = "object",
                Properties = new Dictionary<string, JsonSchema>
                {
                    ["owner"] = new() { Type = "string" },
                    ["repo"] = new() { Type = "string" },
                    ["path"] = new() { Type = "string" }
                },
                Required = new[] { "owner", "repo", "path" }
            }
        };
    }

    public override async Task<ToolResult> CallToolAsync(
        string name,
        JsonElement arguments,
        CancellationToken ct = default)
    {
        return name switch
        {
            "search_repos" => await SearchReposAsync(arguments, ct),
            "get_file" => await GetFileAsync(arguments, ct),
            "create_issue" => await CreateIssueAsync(arguments, ct),
            _ => throw new McpException($"Unknown tool: {name}")
        };
    }

    private async Task<ToolResult> SearchReposAsync(
        JsonElement args,
        CancellationToken ct)
    {
        var query = args.GetProperty("query").GetString()!;
        var limit = args.TryGetProperty("limit", out var l) ? l.GetInt32() : 10;

        var results = await _github.SearchRepositoriesAsync(query, limit, ct);
        return ToolResult.Success(JsonSerializer.Serialize(results));
    }
}

// Run the server
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMcpServer<GitHubMcpServer>();

var host = builder.Build();
await host.RunAsync();

Building an MCP Client

An MCP client discovers and connects to servers, aggregates their tools, and routes requests:

MCP Client Implementation
# MCP Client connecting to multiple servers

class MCPClient:
    servers: Map<string, ServerConnection>

    function initialize(configPath):
        config = loadConfig(configPath)

        for serverConfig in config.mcpServers:
            # Spawn server process
            process = spawn(
                command: serverConfig.command,
                args: serverConfig.args,
                env: serverConfig.env
            )

            # Initialize connection
            connection = connect(process.stdin, process.stdout)

            # Exchange capabilities
            capabilities = connection.initialize({
                clientInfo: { name: "my-agent", version: "1.0" },
                capabilities: {
                    tools: {},
                    resources: {},
                    prompts: {}
                }
            })

            # Store server reference
            servers.set(serverConfig.name, {
                connection: connection,
                capabilities: capabilities,
                tools: capabilities.tools
            })

    function listAllTools():
        allTools = []
        for name, server in servers:
            for tool in server.tools:
                allTools.append({
                    server: name,
                    tool: tool
                })
        return allTools

    function callTool(serverName, toolName, arguments):
        server = servers.get(serverName)
        result = server.connection.callTool(toolName, arguments)
        return result
import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client
from mcp.types import Tool

class MCPClient:
    def __init__(self):
        self.servers: dict[str, ClientSession] = {}

    async def connect_server(
        self,
        name: str,
        command: str,
        args: list[str] | None = None,
        env: dict[str, str] | None = None
    ) -> list[Tool]:
        """Connect to an MCP server and return its tools."""
        server_params = StdioServerParameters(
            command=command,
            args=args or [],
            env=env
        )

        # Create connection
        stdio_transport = await stdio_client(server_params)
        read, write = stdio_transport

        session = ClientSession(read, write)
        await session.initialize()

        # Get server capabilities
        tools_response = await session.list_tools()

        self.servers[name] = session
        return tools_response.tools

    async def call_tool(
        self,
        server_name: str,
        tool_name: str,
        arguments: dict
    ) -> str:
        """Call a tool on a specific server."""
        session = self.servers.get(server_name)
        if not session:
            raise ValueError(f"Server not connected: {server_name}")

        result = await session.call_tool(tool_name, arguments)
        return result.content[0].text

    async def disconnect_all(self):
        """Disconnect from all servers."""
        for session in self.servers.values():
            await session.close()
        self.servers.clear()

# Usage
async def main():
    client = MCPClient()

    # Connect to multiple servers
    github_tools = await client.connect_server(
        name="github",
        command="python",
        args=["-m", "github_mcp_server"]
    )
    print(f"GitHub tools: {[t.name for t in github_tools]}")

    slack_tools = await client.connect_server(
        name="slack",
        command="npx",
        args=["-y", "@anthropic/slack-mcp-server"]
    )
    print(f"Slack tools: {[t.name for t in slack_tools]}")

    # Use tools
    repos = await client.call_tool(
        "github",
        "search_repos",
        {"query": "langchain python"}
    )
    print(repos)

    await client.disconnect_all()

asyncio.run(main())
using ModelContextProtocol.Client;
using System.Diagnostics;

public class McpClientManager : IAsyncDisposable
{
    private readonly Dictionary<string, McpClientSession> _servers = new();

    public async Task<IReadOnlyList<Tool>> ConnectServerAsync(
        string name,
        string command,
        string[]? args = null,
        Dictionary<string, string>? env = null,
        CancellationToken ct = default)
    {
        // Start server process
        var startInfo = new ProcessStartInfo
        {
            FileName = command,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        foreach (var arg in args ?? Array.Empty<string>())
            startInfo.ArgumentList.Add(arg);

        if (env != null)
        {
            foreach (var (key, value) in env)
                startInfo.Environment[key] = value;
        }

        var process = Process.Start(startInfo)
            ?? throw new InvalidOperationException("Failed to start server");

        // Create MCP session
        var session = new McpClientSession(
            process.StandardInput.BaseStream,
            process.StandardOutput.BaseStream
        );

        // Initialize and get capabilities
        var initResult = await session.InitializeAsync(
            new ClientInfo { Name = "my-agent", Version = "1.0" },
            ct
        );

        var toolsResult = await session.ListToolsAsync(ct);

        _servers[name] = session;
        return toolsResult.Tools;
    }

    public async Task<string> CallToolAsync(
        string serverName,
        string toolName,
        JsonElement arguments,
        CancellationToken ct = default)
    {
        if (!_servers.TryGetValue(serverName, out var session))
            throw new InvalidOperationException($"Server not connected: {serverName}");

        var result = await session.CallToolAsync(toolName, arguments, ct);
        return result.Content[0].Text;
    }

    public async ValueTask DisposeAsync()
    {
        foreach (var session in _servers.Values)
        {
            await session.DisposeAsync();
        }
        _servers.Clear();
    }
}

// Usage
await using var client = new McpClientManager();

// Connect to servers
var githubTools = await client.ConnectServerAsync(
    "github",
    "python",
    new[] { "-m", "github_mcp_server" }
);
Console.WriteLine($"GitHub tools: {string.Join(", ", githubTools.Select(t => t.Name))}");

// Call a tool
var result = await client.CallToolAsync(
    "github",
    "search_repos",
    JsonDocument.Parse("{\"query\": \"blazor\"}")
        .RootElement
);
Console.WriteLine(result);

Configuration Format

MCP servers are typically configured via JSON. Here's the standard format used by Claude Desktop and other clients:

MCP Server Configuration
{
  "mcpServers": {
    "github": {
      "command": "python",
      "args": ["-m", "github_mcp_server"],
      "env": {
        "GITHUB_TOKEN": "ghp_xxxx"
      }
    },
    "slack": {
      "command": "npx",
      "args": ["-y", "@anthropic/slack-mcp-server"],
      "env": {
        "SLACK_TOKEN": "xoxb-xxxx"
      }
    },
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@anthropic/filesystem-mcp-server", "/path/to/allowed/dir"]
    },
    "postgres": {
      "command": "python",
      "args": ["-m", "postgres_mcp_server"],
      "env": {
        "DATABASE_URL": "postgresql://user:pass@localhost/db"
      }
    }
  }
}
# ~/.config/claude-desktop/config.json (macOS/Linux)
# %APPDATA%\Claude\config.json (Windows)

{
  "mcpServers": {
    "sqlite": {
      "command": "uvx",
      "args": ["mcp-server-sqlite", "--db-path", "~/data/test.db"]
    },
    "brave-search": {
      "command": "npx",
      "args": ["-y", "@anthropic/brave-search-mcp-server"],
      "env": {
        "BRAVE_API_KEY": "YOUR_API_KEY"
      }
    },
    "memory": {
      "command": "npx",
      "args": ["-y", "@anthropic/memory-mcp-server"]
    }
  }
}

Resources: Read-Only Data Access

Resources provide a way for LLMs to read data without invoking tools. They use URI schemes for addressing:

Pattern Example Use Case
file:// file:///home/user/doc.md Local filesystem access
github:// github://owner/repo/file.py GitHub repository contents
db:// db://mydb/users/123 Database records
api:// api://weather/current/NYC External API data

Resource URI patterns

Implementing Resources
from mcp.server import Server
from mcp.types import Resource, ResourceContents, TextResourceContents

server = Server("data-server")

# List available resources
@server.list_resources()
async def list_resources() -> list[Resource]:
    return [
        Resource(
            uri="data://users",
            name="User Directory",
            description="List of all users in the system",
            mimeType="application/json"
        ),
        Resource(
            uri="data://users/{user_id}",
            name="User Profile",
            description="Detailed profile for a specific user",
            mimeType="application/json"
        ),
        Resource(
            uri="data://reports/{report_type}/{date}",
            name="Reports",
            description="Generated reports by type and date",
            mimeType="application/json"
        )
    ]

# Read a specific resource
@server.read_resource()
async def read_resource(uri: str) -> ResourceContents:
    # Parse URI to extract resource type and parameters
    parts = uri.replace("data://", "").split("/")

    if parts[0] == "users":
        if len(parts) == 1:
            # List all users
            users = await db.get_all_users()
            return TextResourceContents(
                uri=uri,
                mimeType="application/json",
                text=json.dumps(users)
            )
        else:
            # Get specific user
            user_id = parts[1]
            user = await db.get_user(user_id)
            return TextResourceContents(
                uri=uri,
                mimeType="application/json",
                text=json.dumps(user)
            )

    elif parts[0] == "reports":
        report_type = parts[1]
        date = parts[2]
        report = await generate_report(report_type, date)
        return TextResourceContents(
            uri=uri,
            mimeType="application/json",
            text=json.dumps(report)
        )

    raise ValueError(f"Unknown resource: {uri}")

# Subscribe to resource changes (optional)
@server.subscribe_resource()
async def subscribe_resource(uri: str):
    # Set up change notifications
    async def on_change():
        await server.notify_resource_updated(uri)

    await db.watch(uri, on_change)
public class DataMcpServer : McpServer
{
    private readonly IDatabase _db;

    public override IEnumerable<Resource> ListResources()
    {
        yield return new Resource
        {
            Uri = "data://users",
            Name = "User Directory",
            Description = "List of all users in the system",
            MimeType = "application/json"
        };

        yield return new Resource
        {
            Uri = "data://users/{user_id}",
            Name = "User Profile",
            Description = "Detailed profile for a specific user",
            MimeType = "application/json"
        };

        yield return new Resource
        {
            Uri = "data://reports/{report_type}/{date}",
            Name = "Reports",
            Description = "Generated reports by type and date",
            MimeType = "application/json"
        };
    }

    public override async Task<ResourceContents> ReadResourceAsync(
        string uri,
        CancellationToken ct = default)
    {
        var parts = uri.Replace("data://", "").Split('/');

        if (parts[0] == "users")
        {
            if (parts.Length == 1)
            {
                var users = await _db.GetAllUsersAsync(ct);
                return new TextResourceContents
                {
                    Uri = uri,
                    MimeType = "application/json",
                    Text = JsonSerializer.Serialize(users)
                };
            }
            else
            {
                var userId = parts[1];
                var user = await _db.GetUserAsync(userId, ct);
                return new TextResourceContents
                {
                    Uri = uri,
                    MimeType = "application/json",
                    Text = JsonSerializer.Serialize(user)
                };
            }
        }

        if (parts[0] == "reports")
        {
            var reportType = parts[1];
            var date = parts[2];
            var report = await GenerateReportAsync(reportType, date, ct);
            return new TextResourceContents
            {
                Uri = uri,
                MimeType = "application/json",
                Text = JsonSerializer.Serialize(report)
            };
        }

        throw new McpException($"Unknown resource: {uri}");
    }
}

Resources vs Tools

Resources are for reading data (GET operations). Tools are for actions that modify state (POST, PUT, DELETE). Use resources when the LLM just needs to reference data.

Security Considerations

MCP's design includes several security layers. Understanding them is critical for safe deployments:

Layer Protection Implementation
Process Isolation Servers run in separate processes stdio communication, no shared memory
Capability Negotiation Explicit feature opt-in Client declares supported features at init
Argument Validation Type-safe tool inputs JSON Schema validation before execution
Path Scoping Limit filesystem access Whitelist allowed directories
Audit Logging Track all tool invocations Log tool, args, user, timestamp
Security Implementation
# MCP Security Considerations

# 1. TRANSPORT ISOLATION
# Servers run as separate processes
# Communication via stdio (no network exposure)
server = spawn("mcp-server", stdin=PIPE, stdout=PIPE)

# 2. CAPABILITY NEGOTIATION
# Client declares what it supports
# Server only exposes agreed capabilities
capabilities = server.initialize({
    client: { name: "my-app", version: "1.0" },
    capabilities: {
        tools: { listChanged: true },
        resources: { subscribe: false },
        prompts: {}  # Not using prompts
    }
})

# 3. ARGUMENT VALIDATION
# Always validate before passing to tools
function validateToolCall(toolName, arguments):
    schema = getToolSchema(toolName)
    if not validate(arguments, schema):
        return Error("Invalid arguments")
    # Additional domain-specific validation
    if toolName == "delete_file" and arguments.path.startsWith("/"):
        return Error("Absolute paths not allowed")

# 4. RESOURCE SCOPING
# Limit what servers can access
server.config = {
    allowedPaths: ["/home/user/projects"],
    deniedPatterns: ["**/*.env", "**/secrets/*"],
    maxTokensPerRequest: 10000
}

# 5. AUDIT LOGGING
function callTool(server, tool, args):
    log({
        timestamp: now(),
        server: server.name,
        tool: tool,
        arguments: sanitize(args),  # Remove secrets
        user: getCurrentUser()
    })
    return server.callTool(tool, args)
from mcp.server import Server
from mcp.types import Tool
import os
from pathlib import Path

class SecureMcpServer(Server):
    def __init__(self, allowed_paths: list[str]):
        super().__init__("secure-server")
        self.allowed_paths = [Path(p).resolve() for p in allowed_paths]
        self.denied_patterns = ["*.env", "*.key", "secrets/*"]

    def validate_path(self, path: str) -> Path:
        """Ensure path is within allowed directories."""
        resolved = Path(path).resolve()

        # Check if within allowed paths
        is_allowed = any(
            resolved.is_relative_to(allowed)
            for allowed in self.allowed_paths
        )

        if not is_allowed:
            raise PermissionError(f"Path not allowed: {path}")

        # Check denied patterns
        for pattern in self.denied_patterns:
            if resolved.match(pattern):
                raise PermissionError(f"Access denied: {path}")

        return resolved

    @server.call_tool()
    async def call_tool(self, name: str, arguments: dict):
        # Validate all path arguments
        if "path" in arguments:
            arguments["path"] = str(self.validate_path(arguments["path"]))

        if "paths" in arguments:
            arguments["paths"] = [
                str(self.validate_path(p))
                for p in arguments["paths"]
            ]

        # Proceed with validated arguments
        return await self._execute_tool(name, arguments)

# Environment-based configuration
server = SecureMcpServer(
    allowed_paths=os.environ.get("MCP_ALLOWED_PATHS", "").split(":"),
)
public class SecureMcpServer : McpServer
{
    private readonly string[] _allowedPaths;
    private readonly string[] _deniedPatterns = { "*.env", "*.key", "secrets/*" };
    private readonly ILogger _logger;

    public SecureMcpServer(string[] allowedPaths, ILogger logger)
    {
        _allowedPaths = allowedPaths
            .Select(p => Path.GetFullPath(p))
            .ToArray();
        _logger = logger;
    }

    private string ValidatePath(string path)
    {
        var resolved = Path.GetFullPath(path);

        // Check if within allowed paths
        var isAllowed = _allowedPaths.Any(allowed =>
            resolved.StartsWith(allowed, StringComparison.OrdinalIgnoreCase));

        if (!isAllowed)
            throw new UnauthorizedAccessException($"Path not allowed: {path}");

        // Check denied patterns
        foreach (var pattern in _deniedPatterns)
        {
            if (FileSystemName.MatchesSimpleExpression(pattern, resolved))
                throw new UnauthorizedAccessException($"Access denied: {path}");
        }

        return resolved;
    }

    public override async Task<ToolResult> CallToolAsync(
        string name,
        JsonElement arguments,
        CancellationToken ct = default)
    {
        // Audit log
        _logger.LogInformation(
            "Tool call: {Tool} with args: {Args}",
            name,
            SanitizeForLog(arguments)
        );

        // Validate paths in arguments
        var validatedArgs = ValidateArguments(arguments);

        try
        {
            return await ExecuteToolAsync(name, validatedArgs, ct);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Tool {Tool} failed", name);
            throw;
        }
    }

    private string SanitizeForLog(JsonElement args)
    {
        // Remove sensitive fields from logs
        var doc = JsonDocument.Parse(args.GetRawText());
        var sanitized = new Dictionary<string, object>();

        foreach (var prop in doc.RootElement.EnumerateObject())
        {
            if (prop.Name.Contains("token", StringComparison.OrdinalIgnoreCase) ||
                prop.Name.Contains("password", StringComparison.OrdinalIgnoreCase) ||
                prop.Name.Contains("secret", StringComparison.OrdinalIgnoreCase))
            {
                sanitized[prop.Name] = "[REDACTED]";
            }
            else
            {
                sanitized[prop.Name] = prop.Value.ToString();
            }
        }

        return JsonSerializer.Serialize(sanitized);
    }
}

Never Trust User Input

Always validate and sanitize arguments before passing to external systems. MCP tools can be invoked with any arguments the LLM generates.

Secret Management

Store API keys and credentials in environment variables, not in config files. Use secret managers for production deployments.

Available MCP Servers

A growing ecosystem of pre-built MCP servers is available:

Server Tools Provided Package
Filesystem read, write, list, search files @anthropic/filesystem-mcp-server
GitHub repos, issues, PRs, code search @anthropic/github-mcp-server
Slack messages, channels, users @anthropic/slack-mcp-server
PostgreSQL query, schema inspection mcp-server-postgres
SQLite query, schema, modifications mcp-server-sqlite
Brave Search web search, news search @anthropic/brave-search-mcp-server
Memory persistent key-value storage @anthropic/memory-mcp-server
Puppeteer browser automation @anthropic/puppeteer-mcp-server

Community Servers

The MCP ecosystem is growing rapidly. Check github.com/modelcontextprotocol for the latest servers.

Using MCP with Claude

Setup Steps

  1. 1 Install Claude Desktop from claude.ai/download
  2. 2 Create config file at ~/.config/claude-desktop/config.json (macOS/Linux) or %APPDATA%\Claude\config.json (Windows)
  3. 3 Add MCP server configurations (see examples above)
  4. 4 Restart Claude Desktop to load servers
  5. 5 Tools appear in the tools menu (hammer icon)

MCP Apps: Interactive UI Components

MCP Apps is an official extension that enables tools to return interactive UI components (dashboards, forms, visualizations, workflows) that render directly in conversations rather than plain text.

MCP Apps Architecture
┌─────────────────────────────────────────────────────────────────┐
│                      MCP CLIENT (Claude, VS Code)                │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │                    Conversation View                        │ │
│  │                                                             │ │
│  │  User: "Show me Q4 revenue breakdown"                       │ │
│  │                                                             │ │
│  │  Assistant: [Calls analyze_revenue tool]                    │ │
│  │                                                             │ │
│  │  ┌─────────────────────────────────────────────────────┐   │ │
│  │  │           SANDBOXED IFRAME (ui://)                   │   │ │
│  │  │  ┌─────────────────────────────────────────────┐    │   │ │
│  │  │  │  Revenue by Region - Q4 2025               │    │   │ │
│  │  │  │  ┌────┐ ┌────┐ ┌────┐ ┌────┐              │    │   │ │
│  │  │  │  │ NA │ │ EU │ │APAC│ │LATAM│  [Export]   │    │   │ │
│  │  │  │  │████│ │██  │ │███ │ │█   │              │    │   │ │
│  │  │  │  └────┘ └────┘ └────┘ └────┘              │    │   │ │
│  │  │  │  Click any bar to drill down...            │    │   │ │
│  │  │  └─────────────────────────────────────────────┘    │   │ │
│  │  │           ▲                    │                     │   │ │
│  │  │           │ JSON-RPC           │                     │   │ │
│  │  │           └────────────────────┘                     │   │ │
│  │  └─────────────────────────────────────────────────────┘   │ │
│  └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Tool Response Structure:
{
  "content": [{ "type": "text", "text": "Q4 analysis ready" }],
  "_meta": {
    "ui": {
      "resourceUri": "ui://revenue-dashboard",
      "data": { "results": [...], "interactive": true }
    }
  }
}
MCP Apps Implementation
# MCP Apps: Interactive UI Components

# Tool returns UI metadata instead of plain text
function searchAnalytics(query, dateRange):
    results = database.query(query, dateRange)

    # Instead of returning text, return UI reference
    return {
        content: "Found {results.count} matching records",
        _meta: {
            ui: {
                resourceUri: "ui://analytics-dashboard",
                data: {
                    results: results,
                    chartType: "bar",
                    interactive: true
                }
            }
        }
    }

# UI Resource served via ui:// scheme
# Contains sandboxed HTML/JavaScript bundle
resource "ui://analytics-dashboard":
    type: "text/html"
    content: bundledDashboardApp
    permissions: ["display", "user-input"]

# Client renders UI in sandboxed iframe
# Bidirectional JSON-RPC for communication
client.onToolResult = (result) => {
    if result._meta?.ui:
        iframe = createSandboxedIframe(result._meta.ui.resourceUri)
        iframe.postMessage(result._meta.ui.data)
}
import { App } from "@modelcontextprotocol/ext-apps";

// Initialize MCP App client
const app = new App();
await app.connect();

// Handle tool results with UI components
app.ontoolresult = (result) => {
  if (result._meta?.ui) {
    // Render interactive visualization
    renderChart(result.data);
  }
};

// Call server tool that returns UI
const response = await app.callServerTool({
  name: "fetch_analytics",
  arguments: {
    metric: "revenue",
    period: "Q4"
  },
});

// Update model context based on user interaction
// (e.g., user clicks a data point in the chart)
await app.updateModelContext({
  content: [{
    type: "text",
    text: "User selected Q4 revenue breakdown by region"
  }],
});

// Server-side: Tool that returns UI metadata
server.tool("fetch_analytics", async (args) => {
  const data = await analyticsDB.query(args);

  return {
    content: [{
      type: "text",
      text: `Found ${data.length} records for ${args.metric}`
    }],
    _meta: {
      ui: {
        resourceUri: "ui://analytics-chart",
        data: {
          records: data,
          chartConfig: {
            type: "bar",
            interactive: true,
            drilldown: true
          }
        }
      }
    }
  };
});

// Register UI resource
server.resource("ui://analytics-chart", {
  mimeType: "text/html",
  read: async () => {
    return {
      contents: analyticsChartBundle, // Bundled HTML/JS
    };
  }
});
from mcp.server import Server
from mcp.types import Tool, TextContent, Resource

server = Server("analytics-server")

# Tool that returns interactive UI
@server.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "analyze_data":
        results = await db.query(arguments["query"])

        return {
            "content": [
                TextContent(
                    type="text",
                    text=f"Analysis complete: {len(results)} records"
                )
            ],
            "_meta": {
                "ui": {
                    "resourceUri": "ui://data-explorer",
                    "data": {
                        "results": results,
                        "visualization": "interactive-table",
                        "allowExport": True
                    }
                }
            }
        }

# UI Resource definition
@server.read_resource()
async def read_resource(uri: str):
    if uri == "ui://data-explorer":
        # Return bundled HTML/JS application
        return {
            "uri": uri,
            "mimeType": "text/html",
            "text": load_ui_bundle("data-explorer.html")
        }

# List available UI resources
@server.list_resources()
async def list_resources():
    return [
        Resource(
            uri="ui://data-explorer",
            name="Interactive Data Explorer",
            description="Explore and visualize query results",
            mimeType="text/html"
        ),
        Resource(
            uri="ui://config-wizard",
            name="Configuration Wizard",
            description="Step-by-step configuration interface",
            mimeType="text/html"
        )
    ]

Key Benefits

Close the Context Gap

Users see exactly what the tool returns, not just a text summary. Complex data becomes explorable.

Direct Manipulation

Users interact with data directly (click, filter, export) without additional prompts.

Live Updates

UI components can update in real-time as data changes or operations progress.

Persistent State

UI state persists across conversation turns, maintaining context.

Security Model

Layer Protection
Iframe Sandboxing Restricted permissions, isolated execution context
Template Review Pre-declared HTML templates auditable before use
JSON-RPC Messaging All communication auditable, no direct DOM access
User Consent Explicit approval required for tool invocation

MCP Apps security layers

Client Support

MCP Apps is supported in:

  • Claude (web and desktop)
  • Goose
  • Visual Studio Code Insiders
  • ChatGPT (rolling out)

Use Cases

Data Exploration Dashboards Configuration Wizards Document Review Tools Real-time Monitoring Form Builders Workflow Editors

Learn More

See the official announcement: MCP Apps - Interactive UI Components

MCP vs Other Approaches

Approach Pros Cons
MCP Standardized, composable, secure isolation Requires server implementation
Direct API calls Simple for single integrations Custom code per service, no standard
LangChain Tools Rich ecosystem, Python-native Python only, no process isolation
OpenAI Plugins OpenAPI-based, easy to build OpenAI-specific, limited capabilities

Related Topics