はじめに
MCPサーバーといえばPythonかTypeScriptで実装するイメージがありますが、実は公式で Swift SDK も提供されています。
Swiftの強みの一つは、macOSのAPIを簡単に利用できることです。
例えば、NSWorkspaceやAccessibility API、CGWindowList APIなど、macOSのAPIを直接利用できます。他の言語だとAppleScriptを介す必要がありますが、Swiftだとそのようなオーバーヘッドはありません。
今回は仕事効率化のためのMCPサーバーを作りたいと思い、Swiftでこんなものを作ってみました。
AIエージェントに自然言語で指示を出すだけで、アプリの起動やウィンドウ配置ができます!
本記事では、作成したMCPサーバーの機能や具体的なコードを紹介します。
SwiftでMCPサーバーを作るきっかけや参考になれば幸いです。
MCPって何?という方は、みのるんさんの やさしいMCP入門 が非常にわかりやすいのでこちらをオススメします。
対象読者
この記事は以下のような方を対象にしています。
- MCPサーバーを自作してみたい人
- macOSの自動化に興味がある人
- Swiftでツール開発をしてみたい人
きっかけ
仕事効率化への課題
普段の業務でmacOSを使っていて、アプリやウィンドウの管理が煩雑だと感じていました。作業中にアプリを何個も立ち上げっぱなしにしてしまったり、ウィンドウの位置がバラバラになって手動で整えたりすることが多々ありました。
私は基本的にキーボードでPCを操作することが好きなのですが、ことウィンドウの配置においてはどうしてもマウスを使わざるを得なくてなんとかしたいと思っていました。
既存ツールが見つからなかった
macOSを操作するMCPサーバーは他にもありますが、自分の調べた限りアプリとウィンドウの配置・位置管理に特化したものは見当たりませんでした。
参考:macOS操作系のMCPサーバー
- swift-mcp-gui - マウス・キーボード操作(Swift / AppleScript使用)
- mcp-server-macos-use - アプリ操作全般(Swift / Accessibility API)
「ないなら作ろう」ということで、自作することにしました。
MCPサーバーを作ってみたかった
一番の動機はこれですね笑
MCPサーバーの実装言語はPythonやTypeScriptが主流ですが、私はiOSアプリエンジニアとして普段からSwiftを使っています。慣れ親しんだ言語で書けるなら、それが一番効率的ですよね。
また前述のとおり、SwiftだとmacOS APIへのネイティブアクセスが可能であるため生成AIの力を借りれば実装もそこまで難しくないだろうと思っていました。1
機能
今回作ったMCPサーバー「macOS Workspace MCP Server」では、以下の7つのツールを提供しています。
| ツール名 | 機能 |
|---|---|
launch_application |
指定したアプリを起動 |
quit_application |
指定したアプリを終了 |
list_applications |
起動中のアプリ一覧を取得 |
list_windows |
ウィンドウ一覧を取得 |
position_window |
ウィンドウを指定位置に配置 |
focus_window |
指定ウィンドウを最前面に表示 |
list_displays |
接続中のディスプレイ情報を取得 |
実際にClaude Desktopで使ってみると、以下のような感じで操作できます。
また実装はGitHub Repositoryとして公開しています。詳細を確認したい方はこちらを参照いただければと思います。
実装
ここではmacOS Workspace MCP Serverがどのように動いているかを簡単に紹介します。
アーキテクチャ
今回実装したMCPサーバーは以下の4レイヤーで構成されています。
| レイヤー | 役割 |
|---|---|
| Entry Point | MCPサーバー初期化、ハンドラー登録 |
| Tool Registry | ツール一覧管理、ルーティング |
| Tools | MCP入出力処理、引数バリデーション |
| Services | macOS API呼び出し、ビジネスロジック |
以降、各レイヤーのコードを順に見ていきます。
Entry Point(Main.swift)
MCPサーバーの起点となるコードです。
MCP Swift SDKの Server を初期化し、tools/list と tools/call のハンドラーを登録して起動します。
@main
struct MacOSWorkspaceMCPServer {
static func main() async {
do {
let server = await Server(
name: "MacOSWorkspaceMCPServer",
version: "0.1.0",
capabilities: .init(tools: .init(listChanged: false))
)
// ツール一覧を返すハンドラー
.withMethodHandler(ListTools.self) { params in
ToolRegistry.handleListTools(params)
}
// ツール実行ハンドラー
.withMethodHandler(CallTool.self) { params in
await ToolRegistry.handleCallTool(params)
}
// 標準入出力でJSON-RPC通信
let transport = StdioTransport()
try await server.start(transport: transport)
// サーバー実行継続
while true {
try await Task.sleep(for: .seconds(1))
}
} catch {
FileHandle.standardError.write("Error: \(error)\n".data(using: .utf8)!)
exit(1)
}
}
}
Tool Registry(ToolRegistry.swift)
Tool RegistryはMCPツールの一覧管理とルーティングを担当します。
tools/list が呼ばれるとツール定義一覧を返し、tools/call が呼ばれると該当ツールにルーティングします。
public enum ToolRegistry {
/// 登録済みツール一覧
private static let registeredTools: [any MCPTool.Type] = [
LaunchApplicationTool.self,
QuitApplicationTool.self,
ListApplicationsTool.self,
ListWindowsTool.self,
PositionWindowTool.self,
FocusWindowTool.self,
ListDisplaysTool.self,
]
/// tools/list ハンドラー
public static func handleListTools(_ params: ListTools.Parameters) -> ListTools.Result {
let definitions = registeredTools.map { $0.definition }
return .init(tools: definitions, nextCursor: nil)
}
/// tools/call ハンドラー
public static func handleCallTool(_ params: CallTool.Parameters) async -> CallTool.Result {
let arguments = params.arguments ?? [:]
switch params.name {
case LaunchApplicationTool.name:
let service = DefaultApplicationService()
return await LaunchApplicationTool(service: service).execute(arguments: arguments)
// ... 他のツールも同様にルーティング
default:
return .init(content: [.text("Unknown tool: \(params.name)")], isError: true)
}
}
}
Tools(LaunchApplicationTool.swift 等)
各MCPツールの入出力処理を担当するレイヤーです。
各ツールを MCPTool プロトコルに準拠させることで、MCPツールとして必要な情報を定義しながら異なるMCPツールに対して共通の方法で呼び出せるようにしています。
public protocol MCPTool: Sendable {
/// ツール名(MCP準拠のスネークケース)
static var name: String { get }
/// ツール定義(Tool型)
static var definition: Tool { get }
/// ツール実行
/// - Parameter arguments: ツール引数
/// - Returns: 実行結果
func execute(arguments: [String: Value]) async -> CallTool.Result
}
例えば、指定したアプリを起動するツールであるlaunch_applicationの実装は以下のようになります。
public struct LaunchApplicationTool: MCPTool {
public static let name = "launch_application"
public static let definition = Tool(
name: name,
description: """
指定されたbundle IDのアプリケーションを起動します。
アプリが既に起動している場合はアクティブ化(最前面化)します。
成功時にはプロセスIDとアプリケーション名を返します。
""",
inputSchema: .object([
"type": .string("object"),
"properties": .object([
"bundleId": .object([
"type": .string("string"),
"description": .string("アプリケーションのbundle ID(例: com.apple.Safari)"),
])
]),
"required": .array([.string("bundleId")]),
])
)
/// ツール入力パラメーター
struct Input: Decodable {
let bundleId: String?
}
private let service: ApplicationServiceProtocol
public func execute(arguments: [String: Value]) async -> CallTool.Result {
// 1. 引数をデコード
guard let input: Input = MCPArgumentDecoder.decode(from: arguments) else {
return .init(content: [.text("エラー: 引数のデコードに失敗しました。")], isError: true)
}
// 2. バリデーション
guard let bundleId = input.bundleId, !bundleId.isEmpty else {
return .init(content: [.text("パラメーター 'bundleId' が必要です")], isError: true)
}
// 3. サービス呼び出し
do {
let result = try await service.launchApplication(bundleId: bundleId)
let response = LaunchApplicationResponse(from: result)
// 4. 結果をエンコード
return MCPResultEncoder.encode(response)
} catch let error as WorkspaceError {
return .init(content: [.text(error.userMessage)], isError: true)
} catch {
return .init(content: [.text("予期しないエラー: \(error.localizedDescription)")], isError: true)
}
}
}
Services(ApplicationService.swift 等)
Servicesレイヤーは実際のmacOS API呼び出しを担当します。
ツールから分離することでテスト容易性を向上させています。
例えば、Toolsで紹介したlaunch_applicationが使用しているServiceであるApplicationServiceは以下のようになります。
public protocol ApplicationServiceProtocol: Sendable {
func launchApplication(bundleId: String) async throws -> LaunchResult
func quitApplication(bundleId: String) async throws -> QuitResult
func listRunningApplications() async -> [ApplicationInfo]
}
public final class DefaultApplicationService: ApplicationServiceProtocol {
public func launchApplication(bundleId: String) async throws -> LaunchResult {
let workspace = NSWorkspace.shared
// 既に起動しているか確認
if let runningApp = workspace.runningApplications.first(where: {
$0.bundleIdentifier == bundleId
}) {
runningApp.activate() // アクティブ化
return LaunchResult(
processId: Int(runningApp.processIdentifier),
appName: runningApp.localizedName ?? bundleId,
wasAlreadyRunning: true
)
}
// アプリケーションのURLを取得して起動
guard let appURL = workspace.urlForApplication(withBundleIdentifier: bundleId) else {
throw WorkspaceError.applicationNotFound(bundleId: bundleId)
}
let runningApp = try await workspace.openApplication(
at: appURL,
configuration: NSWorkspace.OpenConfiguration()
)
return LaunchResult(
processId: Int(runningApp.processIdentifier),
appName: runningApp.localizedName ?? bundleId,
wasAlreadyRunning: false
)
}
// quitApplication, listRunningApplications も同様に実装
}
使い方
この章は、今回作成したMCPサーバーをとりあえず使ってみたいという方向けの内容です。
興味のある方だけ見ていただき、そうでない方は読み飛ばしていただいても構いません。
前提条件
- macOS 15.0以上
- Swift 6.0
インストール
# リポジトリをクローン
git clone https://github.com/rukawa07/macos-workspace-mcp-server
cd macos-workspace-mcp-server
# ビルド
swift build -c release
MCP Client側の設定
ここではMCP Clientの例として、Claude Desktopの設定ファイルにMCPサーバーを追加する方法を紹介します。
設定ファイルの場所: ~/Library/Application Support/Claude/claude_desktop_config.json
{
"mcpServers": {
"workspace": {
"command": "/path/to/macos-workspace-mcp-server/.build/release/MacOSWorkspaceMCPServer"
}
}
}
/path/to/ の部分は、実際にクローンしたディレクトリのパスに置き換えてください。
アクセシビリティ権限の付与
ウィンドウ操作にはアクセシビリティ権限が必要です。
- システム設定 を開く
- プライバシーとセキュリティ > アクセシビリティ を選択
- Claude Desktop にチェックを入れる2
- Claude Desktopを再起動
おわりに
今回は、SwiftでmacOSのウィンドウ管理MCPサーバーを作ってみました。
所感として、PythonやTypeScriptでなくても公式のSDKさえあれば簡単にMCPサーバーを実装できると感じました。
また、MCPサーバーで何かを実現したいときに、操作対象に適した言語をMCPサーバーの実装言語として選択するのも良いアプローチかなと思いました。
今回作ったMCPサーバーはまだプロトタイプなので、今後の展望としては、複数のアプリやウィンドウを同時に制御する際の速度改善や、iOS SimulatorやGitHub Copilot for Xcodeのようにサイズに制約があるウィンドウへの対応などに取り組めればと考えています。

