DEV Community

K@zuki.
K@zuki.

Posted on • Edited on

How I Simplified My macOS App's AI Integration by Adding a Python Bridge

Hello, I'm @corrupt952 .

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

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:

  1. Non-standard Implementation: My custom HTTP approach didn't align with how most MCP servers operate
  2. Maintenance Burden: Keeping up with MCP protocol changes meant updating Swift code
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Standard Compliance: Claude Desktop sees a standard stdio-based MCP server
  2. Easy Updates: Protocol changes only require updating the Python bridge
  3. Better Testing: Can test the Swift API independently from MCP
  4. 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()
Enter fullscreen mode Exit fullscreen mode

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)}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.