Hello, I'm @corrupt952 .
Previously, I shared how to implement an MCP server in a macOS menu bar application using HTTP/SSE.

Implementing MCP Server in a macOS Menu Bar Application
K@zuki. ・ Jun 10
Today, I'd like to share how that architecture has evolved into something more robust and maintainable using FastMCP.
TL;DR
- Replaced direct HTTP/SSE implementation with a FastMCP-based Python bridge
- Improved security with host validation and access controls
- Achieved better Claude Desktop integration using standard stdio communication
- Maintained the same user experience while gaining ecosystem compatibility
Why Change What Works?
When I first implemented MCP support in Chimr (my macOS calendar notification app), I went with a pure Swift HTTP/SSE server.
It worked, but as I used it more, several issues became apparent:
- Non-standard Implementation: My custom HTTP approach didn't align with how most MCP servers operate
- Maintenance Burden: Keeping up with MCP protocol changes meant updating Swift code
- Limited Ecosystem Integration: Couldn't easily leverage Python MCP tools and libraries
Simply put, I was swimming against the current of the MCP ecosystem.
The New Architecture
Here's how the system works now:
Claude Desktop <--(stdio)--> chimr.py <--(HTTP)--> Swift App
Instead of Claude Desktop talking directly to my Swift HTTP server, it now communicates with a Python-based FastMCP server that acts as a bridge. This might seem like adding complexity, but it actually simplifies things significantly.
Implementing the FastMCP Server
The heart of the new system is chimr.py
. Here's the basic structure:
#!/usr/bin/env uv run --script
# /// script
# dependencies = [
# "mcp",
# "aiohttp"
# ]
# ///
"""
Chimr - MCP server for Chimr calendar integration
"""
import asyncio
from contextlib import asynccontextmanager
from typing import Any, Dict, AsyncIterator
import aiohttp
from mcp.server.fastmcp import FastMCP
# Chimr server configuration
CHIMR_HOST = os.getenv("CHIMR_HOST", "127.0.0.1")
CHIMR_PORT = int(os.getenv("CHIMR_PORT", "8080"))
class ChimrConnection:
def __init__(self, host: str = CHIMR_HOST, port: int = CHIMR_PORT):
self.host = host
self.port = port
self.base_url = f"http://{host}:{port}"
self.session: Optional[aiohttp.ClientSession] = None
async def send_request(self, method: str, params: dict = None) -> dict:
"""Send JSON-RPC request to Chimr server"""
request_data = {
"jsonrpc": "2.0",
"method": method,
"id": 1
}
if params:
request_data["params"] = params
# ... send HTTP request to Swift app
What's beautiful about FastMCP is how it handles all the MCP protocol complexity. I just focus on defining tools:
@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
"""Server lifespan management"""
try:
await chimr_connection.connect()
yield {}
finally:
await chimr_connection.disconnect()
# Create MCP server with proper lifecycle management
mcp = FastMCP(
"Chimr",
description="Chimr calendar integration through the Model Context Protocol",
lifespan=server_lifespan
)
@mcp.tool()
async def get_today_events() -> str:
"""Get today's calendar events from Chimr"""
response = await chimr_connection.send_request(
"tools/call",
{"name": "get_today_events", "arguments": {}}
)
# ... process response
The beauty is that FastMCP handles all the stdio communication, JSON-RPC parsing, and error handling. I just implement the tools.
Swift Side: From Server to Proxy
On the Swift side, the HTTP server remains but with a different purpose. Instead of being the primary MCP server, it's now an internal API that the Python bridge calls:
private func handleHTTPRequest(_ request: HTTPRequest, connection: NWConnection) {
if request.method == "POST" && request.path == "/" {
if let body = request.body, let mcpRequest = MCPRequest(from: body) {
let response = protocolHandler?.handleRequest(mcpRequest)
?? MCPResponse(error: MCPError(code: -32603, message: "Internal error"), id: mcpRequest.id)
sendHTTPResponse(response: response, connection: connection)
}
}
}
But here's where it gets interesting - I added security features that weren't in the original implementation.
Enhanced Security
One concern with running an HTTP server (even on localhost) is security. The new implementation adds several layers of protection:
private func isConnectionAllowed(_ connection: NWConnection) -> Bool {
let settings = AppSettings.shared
// If external access is allowed, accept all connections
if settings.mcpAllowExternalAccess {
return true
}
// Check if the remote endpoint is in the allowed hosts
guard case .hostPort(let host, _) = connection.endpoint else {
return false
}
switch host {
case .ipv4(let ipv4):
let address = ipv4.debugDescription
return settings.mcpAllowedHosts.contains(address)
|| settings.mcpAllowedHosts.contains("localhost")
case .ipv6(let ipv6):
let address = ipv6.debugDescription
return settings.mcpAllowedHosts.contains(address)
|| settings.mcpAllowedHosts.contains("localhost")
case .name(let name, _):
return settings.mcpAllowedHosts.contains(name)
@unknown default:
return false
}
}
This allows users to:
- Restrict connections to localhost only (default)
- Define specific allowed hosts
- Enable external access if needed
Benefits of This Approach
After running this architecture for a while, the benefits are clear:
- Standard Compliance: Claude Desktop sees a standard stdio-based MCP server
- Easy Updates: Protocol changes only require updating the Python bridge
- Better Testing: Can test the Swift API independently from MCP
- Ecosystem Access: Can leverage Python MCP tools and libraries
Implementation Tips
If you're considering a similar architecture for your macOS app, here are some lessons learned:
1. Handle Connection Lifecycle Properly
FastMCP provides lifecycle hooks - use them:
@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
# Setup resources
await setup_connections()
yield {}
# Cleanup
await cleanup_connections()
2. Error Handling at the Bridge
The Python bridge should handle errors gracefully:
try:
response = await chimr_connection.send_request(method, params)
if "result" in response:
return response["result"]
return {"error": "No result returned"}
except Exception as e:
logger.error(f"Error in {method}: {e}")
return {"error": str(e)}
3. Keep the Swift API Simple
Your Swift HTTP server doesn't need all the MCP complexity anymore:
struct MCPRequest {
let method: String
let params: [String: Any]?
let id: Any
}
// Just handle tool calls, not the full MCP protocol
Comparing Architectures
Looking back at both implementations:
Aspect | HTTP/SSE Direct | FastMCP Bridge |
---|---|---|
Complexity | High in Swift | Low in both |
Maintenance | Difficult | Easy |
Standards | Custom | MCP-compliant |
Security | Basic | Enhanced |
Testing | Complex | Modular |
What's Next?
This architecture opens up interesting possibilities:
- Multiple Bridges: Could add Node.js or Rust bridges for different use cases
- Remote Deployment: The Swift app could run on a different machine
- Plugin System: Other apps could integrate with Chimr through the same API
Conclusion
Sometimes the best solution isn't the most direct one. By adding a Python bridge layer, I actually simplified the overall system while gaining standard compliance and better security.
If you're building MCP support into your macOS app, consider whether a bridge architecture might work better than direct implementation. The initial setup might seem more complex, but the long-term benefits are worth it.
Have you implemented MCP in a native application?
What architecture did you choose? Let me know in the comments!
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.