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
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 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 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:
{
"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
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
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 |
# 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
Secret Management
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
Using MCP with Claude
Setup Steps
- 1 Install Claude Desktop from claude.ai/download
- 2 Create config file at
~/.config/claude-desktop/config.json(macOS/Linux) or%APPDATA%\Claude\config.json(Windows) - 3 Add MCP server configurations (see examples above)
- 4 Restart Claude Desktop to load servers
- 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 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: 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
Learn More
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 |