macOS用常駐アプリにMCP Serverを実装する
この記事は部分的にLLMで生成している箇所があります
ネタはたまっているので、どこかのタイミングで放出していきたい K@zuki.です。
今回は久しぶりの投稿になりますが、自作しているmacOSのメニューバー常駐アプリにMCP Serverを実装する話ですが、とにもかくにも見てもらった方が早いので以下をご覧ください。
メニューバー常駐型アプリに実装していて、全画面通知の仕組みも元々あるので、タスクが終わったら全画面通知みたいなこともできる
— K@zuki. (@corrupt952) June 9, 2025
たぶん、こんなことしなくてもApple Script叩くだけのMCPサーバでも似たようなことはできると思われ pic.twitter.com/uzjpYX2Q0c
元々、常駐アプリのフルスクリーン通知で任意の文字列に置き換えれる実装を用意していました。
それをMCP Serverとして利用可能にし、Claude Codeで作業が終わった時の通知として利用可能な状態にしています。
TL;DR
- CLIツールではなくHTTP Serverを常駐アプリに実装
- セキュリティリスクはそこそこあるので対策は検討する必要あり
既存のアプリにMCP Serverを組み込む理由
シンプルに興味関心があったからでしかないです。
一般的なMCP ServerであればCLIツールを書けばいいだけですが、今回のケースは既存のアプリにSSEなMCP Serverを組み込むことで、
- アプリ自体の操作
- アプリの設定変更
といったことをClaude CodeやCursor、Claude DesktopなどのMCP Clinetから呼び出すことができます。
他にもアプリの特定部分のみを流用(フルスクリーン通知など)することができるので、場合によっては便利です。
実装方針
MCP Serverを実装するにはいくつかのパターンがありますが、現時点だと大まかに
- CLI
- HTTP/SSE
の2種類が存在します。
一般的にはCLIでServerを実装しますが、今になって思いましたがCLIで実装しておくのが無難だと思います。
が、常駐アプリ側でListenするのが面倒くさかったのでサクッと作れるHTTP Serverを立てることにしました。
仕組み
大まかな仕組みとしては
- mcp-remoteを呼び出し
- mcp-remote経由で常駐アプリで起動しているHTTP Serverへリクエスト
- 常駐アプリで何らかの処理を行なう
といった流れになっています。 シーケンス図で表現すると以下のような形です。

Claude CodeやClaude Desktopでは、CLIしか対応していないため、mcp-remoteを使ってHTTP Serverと通信する方式をとっています。
実装
SwiftUIで自動起動する場合には以下のように定義しておき、
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() } } */
MCP Serverを以下のように定義しておくと動かすことができます。
コードを見てもらえるとわかると思いますが、想定よりも多くの作業が必要になるので、適宜AIに頼るといいでしょう。
import Foundation import Network // シンプルなMCPサーバーの実装例 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) // HTTPリクエストを読み取る 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)") // SSE接続の場合 if request.contains("GET /sse") { self?.handleSSEConnection(connection) } // JSON-RPCリクエストの場合 else if request.contains("POST /") { self?.handleJSONRPCRequest(connection, requestData: data) } } if isComplete { connection.cancel() } } } private func handleSSEConnection(_ connection: NWConnection) { // SSEヘッダーを送信 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") }) // 初期接続イベントを送信 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) { // HTTPボディを抽出(簡易的な実装) 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() // JSON-RPCリクエストをパース 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 ] } // レスポンスを送信 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() }) } } } } }
Claude Codeから呼び出す
Claude Codeから呼び出す場合には、以下のような設定をします。
{ "mcpServers": { "hello-world": { "command": "npx", "args": [ "mcp-remote", "http://localhost:8080/sse" ] } } }
あとは適切に設定されていれば呼び出すことが可能になり、冒頭でも紹介したようにアプリで用意した機能を利用することができます。
メニューバー常駐型アプリに実装していて、全画面通知の仕組みも元々あるので、タスクが終わったら全画面通知みたいなこともできる
— K@zuki. (@corrupt952) June 9, 2025
たぶん、こんなことしなくてもApple Script叩くだけのMCPサーバでも似たようなことはできると思われ pic.twitter.com/uzjpYX2Q0c
セキュリティ
HTTP Serverとして公開ということはある程度のセキュリティリスクがあることに注意が必要です。
1. ローカルホストバインディング
デフォルトではlocalhost(127.0.0.1)にバインドしていますが、誤って0.0.0.0にバインドすると外部からアクセス可能になります。
// 安全:ローカルホストのみ let listener = try? NWListener(using: parameters, on: NWEndpoint.Port(integerLiteral: port)) // 危険:全てのインターフェース // let listener = try? NWListener(using: parameters, on: NWEndpoint.Port(rawValue: port)!)
2. 認証の欠如
現在の実装では認証機能がないため、ローカルの任意のプロセスがMCPサーバーにアクセスできます。
必要に応じていくつかの認証を検討してください。
3. CORS設定
今回のケースでは考慮しなくても問題ないですが、場合によってはCORSを考える必要があります。
Access-Control-Allow-Originが*は開発時は便利ですが、本番環境では特定のオリジンのみ許可すべきで全開放するのは望ましくありません。
4. 入力検証
JSON-RPCリクエストの入力値を適切に検証し、インジェクション攻撃を防ぐ必要があります。
5. リソース制限
他にもアプリやPCによっては負荷が高まるので注意が必要です。
- 同時接続数の制限
- リクエストサイズの制限
- レート制限
さいごに
今回はmacOS用常駐アプリにMCP Serverを組み込む話でした。
恐らくリリースする頃にはCLIに切り替えているかと思いますが、既存の常駐アプリでMCP ServerのI/Fを用意しておきたい場合の一例にしておいてください。
今回のように実装しておくことで、比較的簡単に自分のアプリをMCP Serverとすることが可能になります。
設定変更の容易化も狙えますし、現段階ではかゆいところに手が届かない機能を利用することや、アプリの特定機能操作も行えるので非常に便利です。
MCP Serverを組み込んでいるアプリは、週末にはベータテスターを募集する予定なので、興味がある人はTwitterかブログをウォッチしておいてください。