iTranslated by AI
Simplifying macOS App AI Integration with a Python Bridge
I'm K@zuki., and I'm starting to take a liking to MUJI's cafe latte base.
Previously, I introduced a way to implement an MCP Server in a macOS menu bar application using HTTP/SSE.
This time, I'd like to share how it evolved into something more robust and maintainable using FastMCP.
TL;DR
- Replaced the direct HTTP/SSE implementation with a FastMCP-based Python bridge
- Improved security through host validation and access control
- Improved integration with Claude Desktop using standard stdio communication
- Gained compatibility with the ecosystem while maintaining the same user experience
Why Change Something That's Working?
When I first implemented MCP support in Chimr, I built a pure Swift HTTP/SSE MCP Server.
While it worked, several issues became apparent as I used it:
- Non-standard implementation ... My custom HTTP approach didn't align with how most MCP Servers operate
- Maintenance burden ... Following changes in the MCP protocol required updating the Swift code
- Limited ecosystem integration ... At the time of implementation, HTTP/SSE was already deprecated, making integration somewhat difficult
Simply put, I was swimming against the tide of the MCP ecosystem.
Therefore, I started implementing it with integration with other MCP Clients in mind.
New Architecture
Here is how the current system operates:
Claude Desktop <--(stdio)--> chimr.py <--(HTTP)--> Swift App
Instead of Claude Desktop communicating directly with the Swift HTTP Server, it now communicates with a Python-based FastMCP Server that acts as a bridge.
While this might seem like adding complexity, it actually simplifies things significantly.
Consequently, it's now available even for MCP Clients that were not supported by the previous HTTP/SSE implementation.
FastMCP Server
The core of the new system is chimr.py. Its basic structure is as follows:
chimr.py
#!/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 connection settings
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"""
request_data = {
"jsonrpc": "2.0",
"method": method,
"id": 1
}
if params:
request_data["params"] = params
# ... Send HTTP request to Swift app
The great thing about FastMCP is that it handles all the complexity of MCP for you.
You can focus on defining tools, making implementation much easier.
Also, as some of you might have noticed, I'm using uv run --script to fetch dependencies dynamically.
While this itself is quite vulnerable to things like supply chain attacks, I recommend it for quick testing because it's so simple.
chimr.py
@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
"""Lifecycle 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 via 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
Since FastMCP handles all stdio communication, JSON-RPC parsing, and error handling, you only need to implement the tools.
While having to define each tool can be a bit tedious, it results in a simple implementation.
Swift
On the Swift side, the HTTP Server remains, but its purpose has changed. Instead of being an HTTP/SSE-based MCP Server as before, it has become an internal API for the Python bridge to call.
Swift - HTTP Server
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)
}
}
}
In this way, it can be a simple implementation that just processes requests from Python and returns responses. This makes it easier to write tests and maintain.
Enhancing Security
One of the concerns when running an HTTP server (even on localhost) is security. In the new implementation, we have added several layers of protection.
Swift - Restricting Connection Sources
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 on an allowed host
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 necessary
Currently, I am considering implementing this via Unix Domain Sockets. Making this the standard would be safer as it prevents access over the network.
Benefits of This Approach
After running this architecture for a while, the benefits are clear:
- Standard compliance ... Common MCP Clients like Claude Desktop can recognize standard stdio-based MCP servers.
- Ease of change ... Protocol changes can be handled by updating the Python bridge.
- Testability ... The Swift API can be tested independently of MCP.
- Access to the ecosystem ... You can leverage Python's MCP tools and libraries.
Implementation Tips
If you are considering implementing an MCP Server with a similar architecture for a macOS app, here are a few lessons I've learned:
Handle Connection Lifecycle Properly
FastMCP provides lifecycle hooks. Use them appropriately.
@asynccontextmanager
async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]:
# Set up resources
await setup_connections()
yield {}
# Clean up
await cleanup_connections()
I occasionally encountered unintended crashes when this was not handled properly. I haven't investigated the cause, so if anyone runs into it, you might want to take a look.
Error Handling in the Bridge
The Python bridge should output errors appropriately to make debugging easier.
try:
response = await chimr_connection.send_request(method, params)
if "result" in response:
return response["result"]
return {"error": "No result was returned"}
except Exception as e:
logger.error(f"Error in {method}: {e}")
return {"error": str(e)}
Keep the Swift API Simple
You no longer need a complex implementation for MCP in the Swift HTTP Server. Just implement it so that it can handle requests from Python.
Swift - Sample Implementation
struct MCPRequest {
let method: String
let params: [String: Any]?
let id: Any
}
class SimpleMCPHandler {
func handleRequest(_ request: MCPRequest) -> [String: Any] {
// Process only tools/call methods
guard request.method == "tools/call",
let params = request.params,
let toolName = params["name"] as? String,
let arguments = params["arguments"] as? [String: Any]
else {
return ["error": "Invalid request"]
}
// Branch processing based on tool name
switch toolName {
case "get_events":
return handleGetEvents(arguments)
case "show_notification":
return handleShowNotification(arguments)
default:
return ["error": "Unknown tool: \(toolName)"]
}
}
private func handleGetEvents(_ args: [String: Any]) -> [String: Any] {
guard let date = args["date"] as? String else {
return ["error": "Missing date parameter"]
}
// Retrieve actual events from CalendarService
let events = CalendarService.shared.getEvents(for: date)
return ["events": events.map { $0.toDictionary() }]
}
private func handleShowNotification(_ args: [String: Any]) -> [String: Any] {
guard let title = args["title"] as? String,
let message = args["message"] as? String
else {
return ["error": "Missing notification parameters"]
}
// Show notification
NotificationService.shared.show(title: title, message: message)
return ["success": true]
}
}
// Usage example in HTTP Server
let handler = SimpleMCPHandler()
// Handle request to POST /api/mcp
if let request = MCPRequest(from: requestData) {
let response = handler.handleRequest(request)
return JSONSerialization.data(withJSONObject: response)
}
Architecture Comparison
Now, looking back at both implementations, we can make the following comparison:
| Aspect | HTTP/SSE | FastMCP Bridge |
|---|---|---|
| Complexity | High in Swift | Low for both MCP/HTTP Server |
| Maintainability | Difficult | Somewhat easy |
| Standard | Custom | MCP compliant |
| Testing | E2E | Modular |
While this table is somewhat subjective, the bridge implementation using FastMCP is very good.
Although Remote MCP is trending, since OAuth or OIDC is a bit overkill for applications running locally, choosing this type of method seems like a good option.
BlenderMCP
By this point, those familiar with MCP Server implementations will likely recognize that I'm referencing the BlenderMCP implementation.
In that project, an HTTP Server runs as a Blender extension, and similarly, a Python MCP Server acts as a bridge.
Claude Desktop <--(stdio)--> BlenderMCP <--(HTTP)--> BlenderMCP Extension --> Blender
Future Outlook
I am considering the following for the future:
- Implementation by purpose/language: By exposing the API, it's possible to implement individual MCP Servers for different purposes or in different languages.
- Remote operation: Enabling the macOS app to be operated from a different machine (dangerous).
- Plugin system: Allowing other apps to integrate with Chimr via the same API (envisioning a future where apps communicate with each other).
Conclusion
Sometimes the best solution isn't the most direct one. By adding a Python bridge layer, I was actually able to simplify the entire system and create a safer, simpler mechanism.
If you're building MCP support into a macOS app, consider whether a bridge architecture might work better than a direct implementation. It's a bit more effort at first, but it's relatively straightforward to implement.
Discussion