macOS Defaults MCP Server
by g0t4
- src
- mcp_server_macos_defaults
import asyncio
import subprocess
from mcp.server.models import InitializationOptions
import mcp.types as types
from mcp.server import NotificationOptions, Server
from pydantic import AnyUrl
import mcp.server.stdio
# Store notes as a simple key-value dict to demonstrate state management
notes: dict[str, str] = {}
server = Server("mcp-server-macos-defaults")
# @server.list_resources()
# async def handle_list_resources() -> list[types.Resource]:
# """
# List available note resources.
# Each note is exposed as a resource with a custom note:// URI scheme.
# """
# return [
# types.Resource(
# uri=AnyUrl(f"note://internal/{name}"),
# name=f"Note: {name}",
# description=f"A simple note named {name}",
# mimeType="text/plain",
# )
# for name in notes
# ]
#
# @server.read_resource()
# async def handle_read_resource(uri: AnyUrl) -> str:
# """
# Read a specific note's content by its URI.
# The note name is extracted from the URI host component.
# """
# if uri.scheme != "note":
# raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
#
# name = uri.path
# if name is not None:
# name = name.lstrip("/")
# return notes[name]
# raise ValueError(f"Note not found: {name}")
#
# @server.list_prompts()
# async def handle_list_prompts() -> list[types.Prompt]:
# """
# List available prompts.
# Each prompt can have optional arguments to customize its behavior.
# """
# return [
# types.Prompt(
# name="summarize-notes",
# description="Creates a summary of all notes",
# arguments=[
# types.PromptArgument(
# name="style",
# description="Style of the summary (brief/detailed)",
# required=False,
# )
# ],
# )
# ]
#
# @server.get_prompt()
# async def handle_get_prompt(
# name: str, arguments: dict[str, str] | None
# ) -> types.GetPromptResult:
# """
# Generate a prompt by combining arguments with server state.
# The prompt includes all current notes and can be customized via arguments.
# """
# if name != "summarize-notes":
# raise ValueError(f"Unknown prompt: {name}")
#
# style = (arguments or {}).get("style", "brief")
# detail_prompt = " Give extensive details." if style == "detailed" else ""
#
# return types.GetPromptResult(
# description="Summarize the current notes",
# messages=[
# types.PromptMessage(
# role="user",
# content=types.TextContent(
# type="text",
# text=f"Here are the current notes to summarize:{detail_prompt}\n\n"
# + "\n".join(
# f"- {name}: {content}"
# for name, content in notes.items()
# ),
# ),
# )
# ],
# )
@server.list_tools()
async def handle_list_tools() -> list[types.Tool]:
"""
List available tools.
Each tool specifies its arguments using JSON Schema validation.
"""
return [
types.Tool(
name="list-domains",
description="List all available macOS domains, same as `defaults domains`",
inputSchema= {
"type": "object",
"properties": {},
}, # TODO filter domains?
),
types.Tool(
name="find",
description="Find entries container given word",
inputSchema= {
"type": "object",
"properties": {
"word": {
"type": "string",
"description": "Word to search for",
},
},
},
),
# defaults read <domain> <key> # key is optional, domain required
types.Tool(
name="defaults-read",
description = "use the `defaults read <domain> <key>` command",
inputSchema = {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Domain to read from",
},
"key": {
"type": "string",
"description": "Key to read from",
},
},
"required": ["domain"],
},
),
# defaults write <domain> <key> <value>
types.Tool(
name="defaults-write",
description = "use the `defaults write <domain> <key> <value>` command",
inputSchema = {
"type": "object",
"properties": {
"domain": {
"type": "string",
"description": "Domain to write to",
},
"key": {
"type": "string",
"description": "Key to write to",
},
"value": {
"type": "string",
"description": "Value to write",
},
},
"required": ["domain", "key", "value"],
},
),
# TODO dictionary values?
# defaults read-type <domain> <key>
# defaults delete <domain> <key> # key is optional, domain required
]
def defaults_read(arguments: dict | None) -> list[types.TextContent]:
if arguments is None:
return []
domain = arguments["domain"]
key = arguments.get("key")
if key is None:
result = subprocess.run(["defaults", "read", domain], capture_output=True)
return [types.TextContent(type="text", text=result.stdout.decode("utf-8"))]
result = subprocess.run(["defaults", "read", domain, key], capture_output=True)
value = result.stdout.decode("utf-8").strip()
return [types.TextContent(type="text", text=f"{key}: {value}")]
def defaults_write(arguments: dict | None) -> list[types.TextContent]:
if arguments is None:
return []
domain = arguments["domain"]
key = arguments["key"]
value = arguments["value"]
# TODO do I need to notify client that resource changed? can't it just ask for it again?
result = subprocess.run(["defaults", "write", domain, key, value], capture_output=True)
return [types.TextContent(type="text", text=result.stdout.decode("utf-8"))]
def list_domains() -> list[types.TextContent]: # get array of domains:
# run command `defaults domains`
result = subprocess.run(["defaults", "domains"], capture_output=True)
domains = result.stdout.decode("utf-8").split(",")
domains = [domain.strip() for domain in domains]
return [types.TextContent(type="text", text=f"Domains: {domains}")]
def find(arguments: dict | None) -> list[types.TextContent]: # ask for help to find a setting, that alone is possibly very useful
if arguments is None:
raise ValueError("Arguments are required")
word = arguments["word"]
result = subprocess.run(["defaults", "find", word], capture_output=True)
return [types.TextContent(type="text", text=f"Found: {result.stdout.decode('utf-8')}")]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict | None) -> list[types.TextContent]:
# TODO => other types? | types.ImageContent | types.EmbeddedResource]:
# # Notify clients that resources have changed (if server changes resources)
# await server.request_context.session.send_resource_list_changed()
if name == "list-domains":
return list_domains()
elif name == "find":
return find(arguments)
elif name == "defaults-read":
return defaults_read(arguments)
elif name == "defaults-write":
return defaults_write(arguments)
else:
raise ValueError(f"Unknown tool: {name}")
async def main():
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="mcp-server-macos-defaults",
server_version="0.1.2",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
)