Implementing MCP Server in a macOS Menu Bar Application
K@zuki.

K@zuki. @corrupt952

About: Programmer

Location:
Japan
Joined:
Oct 30, 2022

Implementing MCP Server in a macOS Menu Bar Application

Publish Date: Jun 10
0 2

This article contains some sections partially generated by LLM

Hello, I'm K@zuki, and I have some content stockpiled that I'd like to release at some point.

This is my first post in a while, and it's about implementing an MCP Server in a custom macOS menu bar resident app I've been developing. But rather than explain everything upfront, it's better to just show you what I've built:

Originally, I had implemented functionality in the resident app that could replace arbitrary strings with fullscreen notifications. I've now made this available as an MCP Server, allowing it to be used as a notification system when work is completed in Claude Code.

TL;DR

  • Implemented HTTP Server instead of CLI tool in the resident app
  • There are some security risks that need to be considered and addressed

Why Integrate MCP Server into an Existing App

Simply put, it was driven by curiosity and interest. While a typical MCP Server would just require writing a CLI tool, in this case, by integrating an SSE-based MCP Server into an existing app, I can:

  • Control the app itself
  • Change app settings

These operations can be called from MCP Clients like Claude Code, Cursor, or Claude Desktop. Additionally, I can leverage specific parts of the app (like fullscreen notifications), which can be quite convenient in certain scenarios.

Implementation Approach

There are several patterns for implementing MCP Server, but currently there are roughly two main types:

  • CLI
  • HTTP/SSE

Generally, servers are implemented as CLI tools, and thinking about it now, implementing as CLI would probably be the safer choice. However, since setting up listening on the resident app side seemed cumbersome, I decided to quickly set up an HTTP Server instead.

How It Works

The general mechanism works as follows:

  1. Call mcp-remote
  2. Send requests to the HTTP Server running in the resident app via mcp-remote
  3. The resident app performs some processing

The flow can be represented in the following sequence diagram:

Image description

Since Claude Code and Claude Desktop only support CLI, I'm using mcp-remote to communicate with the HTTP Server.

Implementation

For auto-startup with SwiftUI, define it as follows:

import SwiftUI

@main
struct MyApp: App {
    @StateObject private var mcpServer = SimpleMCPServerWrapper()

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class SimpleMCPServerWrapper: ObservableObject {
    private let server = SimpleMCPServer()

    init() {
        server.start()
    }

    deinit {
        server.stop()
    }
}
Enter fullscreen mode Exit fullscreen mode

You can make it work by defining the MCP Server as follows. As you can see from the code, this requires more work than initially expected, so it's good to rely on AI assistance as needed.

import Foundation
import Network

// Simple MCP Server implementation example
class SimpleMCPServer {
    private var listener: NWListener?
    private let port: UInt16 = 8080

    func start() {
        let parameters = NWParameters.tcp
        parameters.allowLocalEndpointReuse = true

        guard let listener = try? NWListener(using: parameters, on: NWEndpoint.Port(integerLiteral: port)) else {
            print("Failed to create listener")
            return
        }

        self.listener = listener

        listener.newConnectionHandler = { [weak self] connection in
            self?.handleConnection(connection)
        }

        listener.start(queue: .main)
        print("MCP Server started on port \(port)")
    }

    func stop() {
        listener?.cancel()
        listener = nil
        print("MCP Server stopped")
    }

    private func handleConnection(_ connection: NWConnection) {
        connection.start(queue: .main)

        // Read HTTP request
        connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] data, _, isComplete, error in
            guard let data = data, error == nil else {
                connection.cancel()
                return
            }

            if let request = String(data: data, encoding: .utf8) {
                print("Received request:\n\(request)")

                // Handle SSE connection
                if request.contains("GET /sse") {
                    self?.handleSSEConnection(connection)
                }
                // Handle JSON-RPC request
                else if request.contains("POST /") {
                    self?.handleJSONRPCRequest(connection, requestData: data)
                }
            }

            if isComplete {
                connection.cancel()
            }
        }
    }

    private func handleSSEConnection(_ connection: NWConnection) {
        // Send SSE headers
        let headers = """
        HTTP/1.1 200 OK\r
        Content-Type: text/event-stream\r
        Cache-Control: no-cache\r
        Connection: keep-alive\r
        Access-Control-Allow-Origin: *\r
        \r

        """

        connection.send(content: headers.data(using: .utf8), completion: .contentProcessed { _ in
            print("SSE connection established")
        })

        // Send initial connection event
        let connectEvent = "event: connected\ndata: {\"status\": \"connected\"}\n\n"
        connection.send(content: connectEvent.data(using: .utf8), completion: .contentProcessed { _ in })
    }

    private func handleJSONRPCRequest(_ connection: NWConnection, requestData: Data) {
        // Extract HTTP body (simplified implementation)
        if let requestString = String(data: requestData, encoding: .utf8),
           let bodyStart = requestString.range(of: "\r\n\r\n") {
            let bodyData = String(requestString[bodyStart.upperBound...]).data(using: .utf8) ?? Data()

            // Parse JSON-RPC request
            if let json = try? JSONSerialization.jsonObject(with: bodyData) as? [String: Any],
               let method = json["method"] as? String,
               let id = json["id"] {

                let response: [String: Any]

                switch method {
                case "initialize":
                    response = [
                        "jsonrpc": "2.0",
                        "result": [
                            "protocolVersion": "2024-11-05",
                            "serverInfo": [
                                "name": "simple-mcp-server",
                                "version": "1.0.0"
                            ],
                            "capabilities": [
                                "tools": [:],
                                "resources": [:]
                            ]
                        ],
                        "id": id
                    ]

                case "tools/list":
                    response = [
                        "jsonrpc": "2.0",
                        "result": [
                            "tools": [[
                                "name": "hello",
                                "description": "Returns Hello, World!",
                                "inputSchema": [
                                    "type": "object",
                                    "properties": [:]
                                ]
                            ]]
                        ],
                        "id": id
                    ]

                case "tools/call":
                    if let params = json["params"] as? [String: Any],
                       let toolName = params["name"] as? String,
                       toolName == "hello" {
                        response = [
                            "jsonrpc": "2.0",
                            "result": [
                                "content": [[
                                    "type": "text",
                                    "text": "Hello, World! from MCP Server"
                                ]]
                            ],
                            "id": id
                        ]
                    } else {
                        response = [
                            "jsonrpc": "2.0",
                            "error": [
                                "code": -32601,
                                "message": "Method not found"
                            ],
                            "id": id
                        ]
                    }

                default:
                    response = [
                        "jsonrpc": "2.0",
                        "error": [
                            "code": -32601,
                            "message": "Method not found"
                        ],
                        "id": id
                    ]
                }

                // Send response
                if let responseData = try? JSONSerialization.data(withJSONObject: response),
                   let responseString = String(data: responseData, encoding: .utf8) {

                    let httpResponse = """
                    HTTP/1.1 200 OK\r
                    Content-Type: application/json\r
                    Content-Length: \(responseData.count)\r
                    Access-Control-Allow-Origin: *\r
                    \r
                    \(responseString)
                    """

                    connection.send(content: httpResponse.data(using: .utf8), completion: .contentProcessed { _ in
                        connection.cancel()
                    })
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Calling from Claude Code

To call from Claude Code, configure it as follows:

{
  "mcpServers": {
    "hello-world": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "http://localhost:8080/sse"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Once properly configured, you can call it and use the functionality prepared in the app, as shown in the introduction:

Security Considerations

Publishing as an HTTP Server means there are certain security risks that need attention.

1. Localhost Binding

By default, it binds to localhost (127.0.0.1), but accidentally binding to 0.0.0.0 would make it accessible from external sources.

// Safe: localhost only
let listener = try? NWListener(using: parameters, on: NWEndpoint.Port(integerLiteral: port))

// Dangerous: all interfaces
// let listener = try? NWListener(using: parameters, on: NWEndpoint.Port(rawValue: port)!)
Enter fullscreen mode Exit fullscreen mode

2. Lack of Authentication

The current implementation has no authentication functionality, meaning any local process can access the MCP server. Consider implementing authentication as needed:

  • API key-based authentication
  • Token-based authentication
  • Connection source process verification

3. CORS Settings

While not a concern in this case, CORS may need consideration in some scenarios. Access-Control-Allow-Origin: * is convenient for development, but in production environments, you should only allow specific origins rather than leaving it wide open.

4. Input Validation

Properly validate JSON-RPC request input values to prevent injection attacks.

5. Resource Limits

Depending on the app and PC, high load may be a concern, so pay attention to:

  • Concurrent connection limits
  • Request size limits
  • Rate limiting

Conclusion

This post covered integrating MCP Server into a macOS resident app. I'll probably switch to CLI by the time I release it, but please use this as an example for cases where you want to prepare an MCP Server interface for existing resident apps.

By implementing it this way, you can relatively easily turn your own app into an MCP Server. You can also aim for easier configuration changes, utilize features that currently don't reach those itchy spots, and control specific app functions, making it very convenient.

The app with integrated MCP Server is planned to recruit beta testers over the weekend, so if you're interested, keep an eye on Twitter or the blog.

Comments 2 total

  • АнонимJun 10, 2025

    [hidden by post author]

  • Richard
    RichardJun 11, 2025

    Hey! redeem your exclusive around $15 in BNB-based tokens today! — Join now! Only available to connected crypto wallets. 👉 duckybsc.xyz

Add comment