Python Server Walkthrough
This guide provides a step-by-step walkthrough for creating an MCP server with UI resources using mcp-ui-server
and FastMCP.
For a complete, runnable example, see the python-server-demo
.
1. Set up Your Python Environment
First, create a new Python project and set up your dependencies:
# Create a new directory
mkdir my-mcp-server
cd my-mcp-server
# Initialize with uv (recommended) or pip
uv init
# or: python -m venv venv && source venv/bin/activate
2
3
4
5
6
7
2. Install Dependencies
Install the necessary packages:
uv add mcp mcp-ui-server
Or with pip:
pip install mcp mcp-ui-server
The mcp
package provides FastMCP and core MCP functionality, while mcp-ui-server
includes helpers for creating UI resources.
3. Create Your MCP Server
Create a file called server.py
:
import argparse
from mcp.server.fastmcp import FastMCP
from mcp_ui_server import create_ui_resource
from mcp_ui_server.core import UIResource
# Create FastMCP instance
mcp = FastMCP("my-mcp-server")
@mcp.tool()
def greet() -> list[UIResource]:
"""A simple greeting tool that returns a UI resource."""
ui_resource = create_ui_resource({
"uri": "ui://greeting/simple",
"content": {
"type": "rawHtml",
"htmlString": """
<div style="padding: 20px; text-align: center; font-family: Arial, sans-serif;">
<h1 style="color: #2563eb;">Hello from Python MCP Server!</h1>
<p>This UI resource was generated server-side using mcp-ui-server.</p>
</div>
"""
},
"encoding": "text"
})
return [ui_resource]
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="My MCP Server")
parser.add_argument("--http", action="store_true", help="Use HTTP transport instead of stdio")
parser.add_argument("--port", type=int, default=3000, help="Port for HTTP transport (default: 3000)")
args = parser.parse_args()
if args.http:
print("🚀 Starting MCP server on HTTP (SSE transport)")
mcp.settings.port = args.port
mcp.run(transport="sse")
else:
print("🚀 Starting MCP server with stdio transport")
mcp.run()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
4. Add More UI Tools
Let's add more sophisticated tools with different types of UI resources:
@mcp.tool()
def show_dashboard() -> list[UIResource]:
"""Display a sample dashboard with metrics."""
dashboard_html = """
<div style="padding: 20px; font-family: Arial, sans-serif;">
<h1>Server Dashboard</h1>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 20px;">
<div style="background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 8px; padding: 16px;">
<h3 style="margin-top: 0; color: #0369a1;">Active Connections</h3>
<p style="font-size: 24px; font-weight: bold; margin: 0; color: #0c4a6e;">42</p>
</div>
<div style="background: #f0fdf4; border: 1px solid #22c55e; border-radius: 8px; padding: 16px;">
<h3 style="margin-top: 0; color: #15803d;">CPU Usage</h3>
<p style="font-size: 24px; font-weight: bold; margin: 0; color: #14532d;">23%</p>
</div>
<div style="background: #fefce8; border: 1px solid #eab308; border-radius: 8px; padding: 16px;">
<h3 style="margin-top: 0; color: #a16207;">Memory Usage</h3>
<p style="font-size: 24px; font-weight: bold; margin: 0; color: #713f12;">67%</p>
</div>
</div>
</div>
"""
ui_resource = create_ui_resource({
"uri": "ui://dashboard/main",
"content": {
"type": "rawHtml",
"htmlString": dashboard_html
},
"encoding": "text"
})
return [ui_resource]
@mcp.tool()
def show_external_site() -> list[UIResource]:
"""Display an external website in an iframe."""
ui_resource = create_ui_resource({
"uri": "ui://external/example",
"content": {
"type": "externalUrl",
"iframeUrl": "https://example.com"
},
"encoding": "text"
})
return [ui_resource]
@mcp.tool()
def show_interactive_demo() -> list[UIResource]:
"""Show an interactive demo with buttons that send intents."""
interactive_html = """
<div style="padding: 20px; font-family: Arial, sans-serif;">
<h2>Interactive Demo</h2>
<p>Click the buttons below to send different types of actions back to the parent:</p>
<div style="margin: 10px 0;">
<button onclick="sendIntent('user_action', {type: 'button_click', id: 'demo'})"
style="background: #2563eb; color: white; padding: 8px 16px; border: none; border-radius: 4px; margin: 5px; cursor: pointer;">
Send Intent
</button>
<button onclick="sendToolCall('get_data', {source: 'ui'})"
style="background: #059669; color: white; padding: 8px 16px; border: none; border-radius: 4px; margin: 5px; cursor: pointer;">
Call Tool
</button>
</div>
<div id="status" style="margin-top: 20px; padding: 10px; background: #f3f4f6; border-radius: 4px;">
Ready - click a button to see the action
</div>
</div>
<script>
function sendIntent(intent, params) {
const status = document.getElementById('status');
status.innerHTML = `<strong>Intent sent:</strong> ${intent}<br><strong>Params:</strong> ${JSON.stringify(params)}`;
if (window.parent) {
window.parent.postMessage({
type: 'intent',
payload: { intent: intent, params: params }
}, '*');
}
}
function sendToolCall(toolName, params) {
const status = document.getElementById('status');
status.innerHTML = `<strong>Tool call:</strong> ${toolName}<br><strong>Params:</strong> ${JSON.stringify(params)}`;
if (window.parent) {
window.parent.postMessage({
type: 'tool',
payload: { toolName: toolName, params: params }
}, '*');
}
}
</script>
"""
ui_resource = create_ui_resource({
"uri": "ui://demo/interactive",
"content": {
"type": "rawHtml",
"htmlString": interactive_html
},
"encoding": "text"
})
return [ui_resource]
@mcp.tool()
def show_remote_dom() -> list[UIResource]:
"""Show a Remote DOM component with dynamic elements."""
remote_dom_script = """
const container = document.createElement('div');
container.style.padding = '20px';
container.style.fontFamily = 'Arial, sans-serif';
const title = document.createElement('ui-text');
title.textContent = 'Remote DOM Component';
title.style.fontSize = '18px';
title.style.fontWeight = 'bold';
title.style.marginBottom = '10px';
const description = document.createElement('ui-text');
description.textContent = 'This component was created using Remote DOM scripting.';
description.style.marginBottom = '15px';
const button = document.createElement('ui-button');
button.setAttribute('label', 'Click for Tool Call');
button.addEventListener('press', () => {
window.parent.postMessage({
type: 'tool',
payload: {
toolName: 'handle_remote_dom_action',
params: { source: 'remote-dom', timestamp: Date.now() }
}
}, '*');
});
container.appendChild(title);
container.appendChild(description);
container.appendChild(button);
root.appendChild(container);
"""
ui_resource = create_ui_resource({
"uri": "ui://remote-dom/demo",
"content": {
"type": "remoteDom",
"script": remote_dom_script.strip(),
"framework": "react"
},
"encoding": "text"
})
return [ui_resource]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
5. Complete Server Example
Here's your complete server.py
file:
import argparse
from mcp.server.fastmcp import FastMCP
from mcp_ui_server import create_ui_resource
from mcp_ui_server.core import UIResource
# Create FastMCP instance
mcp = FastMCP("my-mcp-server")
@mcp.tool()
def greet() -> list[UIResource]:
"""A simple greeting tool that returns a UI resource."""
ui_resource = create_ui_resource({
"uri": "ui://greeting/simple",
"content": {
"type": "rawHtml",
"htmlString": """
<div style="padding: 20px; text-align: center; font-family: Arial, sans-serif;">
<h1 style="color: #2563eb;">Hello from Python MCP Server!</h1>
<p>This UI resource was generated server-side using mcp-ui-server.</p>
</div>
"""
},
"encoding": "text"
})
return [ui_resource]
@mcp.tool()
def show_dashboard() -> list[UIResource]:
"""Display a sample dashboard with metrics."""
dashboard_html = """
<div style="padding: 20px; font-family: Arial, sans-serif;">
<h1>Server Dashboard</h1>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-top: 20px;">
<div style="background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 8px; padding: 16px;">
<h3 style="margin-top: 0; color: #0369a1;">Active Connections</h3>
<p style="font-size: 24px; font-weight: bold; margin: 0; color: #0c4a6e;">42</p>
</div>
<div style="background: #f0fdf4; border: 1px solid #22c55e; border-radius: 8px; padding: 16px;">
<h3 style="margin-top: 0; color: #15803d;">CPU Usage</h3>
<p style="font-size: 24px; font-weight: bold; margin: 0; color: #14532d;">23%</p>
</div>
</div>
</div>
"""
ui_resource = create_ui_resource({
"uri": "ui://dashboard/main",
"content": {
"type": "rawHtml",
"htmlString": dashboard_html
},
"encoding": "text"
})
return [ui_resource]
@mcp.tool()
def show_external_site() -> list[UIResource]:
"""Display an external website in an iframe."""
ui_resource = create_ui_resource({
"uri": "ui://external/example",
"content": {
"type": "externalUrl",
"iframeUrl": "https://example.com"
},
"encoding": "text"
})
return [ui_resource]
@mcp.tool()
def show_interactive_demo() -> list[UIResource]:
"""Show an interactive demo with buttons that send intents."""
interactive_html = """
<div style="padding: 20px; font-family: Arial, sans-serif;">
<h2>Interactive Demo</h2>
<p>Click the button below to send an intent back to the parent:</p>
<button onclick="sendIntent()"
style="background: #2563eb; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer;">
Send Intent
</button>
<div id="status" style="margin-top: 20px; padding: 10px; background: #f3f4f6; border-radius: 4px;">
Ready
</div>
</div>
<script>
function sendIntent() {
const status = document.getElementById('status');
status.innerHTML = 'Intent sent!';
if (window.parent) {
window.parent.postMessage({
type: 'intent',
payload: {
intent: 'demo_interaction',
params: { source: 'python-server', timestamp: new Date().toISOString() }
}
}, '*');
}
}
</script>
"""
ui_resource = create_ui_resource({
"uri": "ui://demo/interactive",
"content": {
"type": "rawHtml",
"htmlString": interactive_html
},
"encoding": "text"
})
return [ui_resource]
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="My MCP Server")
parser.add_argument("--http", action="store_true", help="Use HTTP transport instead of stdio")
parser.add_argument("--port", type=int, default=3000, help="Port for HTTP transport (default: 3000)")
args = parser.parse_args()
if args.http:
print("🚀 Starting MCP server on HTTP (SSE transport)")
print("📡 Server will use SSE transport settings")
mcp.settings.port = args.port
mcp.run(transport="sse")
else:
print("🚀 Starting MCP server with stdio transport")
mcp.run()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
6. Run Your Server
You can run your server in two modes:
Stdio Mode (for command-line clients):
python server.py
HTTP Mode (for web clients):
python server.py --http --port 3000
7. Test with UI Inspector
To test your server with a visual interface:
- Go to the ui-inspector repository and run it locally
- Open the inspector in your browser (usually
http://localhost:6274
) - Configure the connection:
- Transport Type: "SSE" (for HTTP mode) or "Stdio" (for stdio mode)
- Server URL:
http://localhost:3000/mcp
(for HTTP mode)
- Click "Connect"
The inspector will show your tools:
- greet: Simple HTML greeting
- show_dashboard: Dashboard with metrics
- show_external_site: External website iframe
- show_interactive_demo: Interactive buttons with intents
When you call these tools, the UI resources will be rendered in the inspector's Tool Results panel.
8. Next Steps
Now that you have a working MCP server with UI resources, you can:
- Add more tools with different types of content
- Handle user interactions by implementing tools that respond to intents
- Create dynamic content based on tool parameters
- Integrate with external APIs to display live data
- Use blob encoding for larger or binary content
For more examples and advanced usage, see the Usage Examples documentation.
Tips for Development
- Use
encoding: "text"
for simple HTML content - Use
encoding: "blob"
for larger content or when you need Base64 encoding - Always prefix URIs with
ui://
followed by your component identifier - Test both stdio and HTTP transports depending on your use case
- Use the ui-inspector for visual testing and debugging
You've successfully created a Python MCP server with UI resources! The FastMCP framework makes it easy to create tools that return rich, interactive UI content to MCP clients.