iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🐡

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.

https://khasegawa.hatenablog.com/entry/2025/06/10/205422

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:

  1. Non-standard implementation ... My custom HTTP approach didn't align with how most MCP Servers operate
  2. Maintenance burden ... Following changes in the MCP protocol required updating the Swift code
  3. 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.

https://chimr.zuki.dev/

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:

  1. Standard compliance ... Common MCP Clients like Claude Desktop can recognize standard stdio-based MCP servers.
  2. Ease of change ... Protocol changes can be handled by updating the Python bridge.
  3. Testability ... The Swift API can be tested independently of MCP.
  4. 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.

https://github.com/ahujasid/blender-mcp

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