MCPs Part 2: Building an App Opener MCP Server for macOS

Open LLM-readable version of this post

Learn how to build a custom MCP server that allows Claude Desktop to open applications on your macOS system. This step-by-step guide shows you how to create a tool that brings AI assistance to your daily workflow.

MCPs Part 2: Building an App Opener MCP Server for macOS

The Model Context Protocol (MCP) has revolutionized the way we interact with Large Language Models, enabling them to access tools, data, and structured prompts. In my previous tutorial, we explored how to build a Hello World MCP server with basic capabilities. Today, we’ll go a step further by creating a practical MCP server that allows Claude Desktop to open applications on your macOS system.

Why an App Opener MCP Server?

Having Claude open applications for you might seem like a small convenience, but it demonstrates a fundamental aspect of AI assistants: the ability to interact with your operating system. With this capability, you can:

  1. Streamline your workflow: Ask Claude to open your design tools while discussing a project
  2. Chain operations: Have Claude open an app and then perform actions with it
  3. Build more complex automations: This serves as a foundation for more advanced system interactions

Let’s dive into building this practical tool.

Prerequisites

Before we start, ensure you have:

  • macOS operating system
  • Claude Desktop app installed
  • Node.js and npm installed
  • Basic knowledge of TypeScript and terminal commands

Setting Up the Project

Let’s create our project structure:

mkdir claude-app-opener
cd claude-app-opener
npm init -y
npm install @modelcontextprotocol/sdk zod express
npm install -D typescript @types/node @types/express

Create a tsconfig.json file:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Update your package.json to include build and start scripts:

{
  "name": "claude-app-opener",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.1.0",
    "zod": "^3.22.4",
    "express": "^4.18.2"
  },
  "devDependencies": {
    "@types/node": "^22.10.5",
    "@types/express": "^4.17.21",
    "typescript": "^5.7.2"
  }
}

Building the App Opener Tool

Now, let’s create our src/index.ts file with the MCP server implementation:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { exec } from "child_process";
import { promisify } from "util";

// Promisify exec to use async/await
const execAsync = promisify(exec);

// Initialize server
const server = new McpServer({
  name: "app-opener",
  version: "1.0.0"
});

// Define a list of common macOS applications
const commonApps = [
  "Safari",
  "Chrome",
  "Firefox",
  "Mail",
  "Calendar",
  "Notes",
  "Photos",
  "Music",
  "Messages",
  "FaceTime",
  "Maps",
  "Keynote",
  "Pages",
  "Numbers",
  "Preview",
  "Terminal",
  "Visual Studio Code",
  "Xcode",
  "Finder"
];

// Define the app opener tool
server.tool(
  "app-opener",
  "Open applications on macOS",
  {
    app_name: z.string({
      description: "The name of the application to open (e.g., 'Safari', 'Spotify', 'Visual Studio Code')"
    })
  },
  async ({ app_name }) => {
    try {
      // Sanitize the app name to prevent command injection
      const sanitizedAppName = app_name.replace(/[;&|<>()$`\\"\'\*]/g, '');
      
      // Execute the open command to launch the app
      const { stdout, stderr } = await execAsync(`open -a "${sanitizedAppName}"`);
      
      if (stderr) {
        return {
          content: [{
            type: "text",
            text: `Error opening application: ${stderr}`
          }]
        };
      }
      
      return {
        content: [{
          type: "text",
          text: `Successfully opened ${sanitizedAppName}.`
        }]
      };
    } catch (error) {
      return {
        content: [{
          type: "text",
          text: `Failed to open ${app_name}. The application might not be installed or has a different name.`
        }]
      };
    }
  }
);

// Define a resource to list commonly available applications
server.resource(
  "common-apps",
  "apps://common",
  async (uri) => ({
    contents: [{
      uri: uri.href,
      text: `Common macOS Applications:\n${commonApps.join('\n')}`
    }]
  })
);

// Start server using stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.info('{"jsonrpc": "2.0", "method": "log", "params": { "message": "App Opener server running..." }}');

Safety Considerations

The app opener tool includes several safety features:

  1. Input sanitization: We remove special characters that could be used for command injection
  2. Limited functionality: The tool only opens applications, minimizing potential risks
  3. Approval workflow: Claude will ask for your permission before executing the command

These precautions ensure the tool is safe for everyday use.

Building and Testing the Server

Let’s build our server:

npm run build

To manually test the server with the MCP Inspector:

npx @modelcontextprotocol/inspector node build/index.js

In the Inspector:

  1. Go to the “Tools” tab
  2. Click “List Tools” to see your app-opener tool
  3. Click on the tool, enter an application name (e.g., “Safari”)
  4. Click “Execute” to see if it opens the application

Integrating with Claude Desktop

Now let’s set up Claude Desktop to use our new MCP server:

  1. First, determine the absolute path to your project:
pwd

This will output something like /Users/username/projects/claude-app-opener.

  1. Create or edit the Claude Desktop configuration file:

For macOS, the file is located at:

~/Library/Application\ Support/Claude/claude_desktop_config.json

If the file doesn’t exist, create it with the following content (replace /Users/username/projects/claude-app-opener with your actual path):

{
  "mcpServers": {
    "app-opener": {
      "command": "node",
      "args": ["/Users/username/projects/claude-app-opener/build/index.js"]
    }
  }
}
  1. Restart Claude Desktop to apply the changes.

Using the App Opener with Claude

Once the server is set up, you can start using Claude Desktop to open applications. Here are some example prompts:

  • “Can you open Safari for me?”
  • “I need to check my email. Please open the Mail application.”
  • “Open Visual Studio Code so I can work on my project.”
  • “What applications can you open? Then open the Calendar app for me.”

When Claude recognizes a request to open an application, it will:

  1. Show you the command it plans to execute
  2. Ask for your permission
  3. Execute the command only after your approval
  4. Confirm the application has been opened

Extending the App Opener

This basic implementation can be extended in several ways:

Adding Application Discovery

You could enhance the server to dynamically discover installed applications:

// Add a function to find installed applications
async function getInstalledApps() {
  try {
    const { stdout } = await execAsync('ls -1 /Applications | grep .app | sed "s/.app//g"');
    return stdout.split('\n').filter(app => app.trim() !== '');
  } catch (error) {
    return commonApps;
  }
}

// Update the resource to use dynamically discovered apps
server.resource(
  "installed-apps",
  "apps://installed",
  async (uri) => {
    const installedApps = await getInstalledApps();
    return {
      contents: [{
        uri: uri.href,
        text: `Installed Applications:\n${installedApps.join('\n')}`
      }]
    };
  }
);

Adding App Launch with Parameters

You could extend the tool to open applications with specific files or URLs:

server.tool(
  "app-opener-with-params",
  "Open applications with parameters on macOS",
  {
    app_name: z.string({
      description: "The name of the application to open"
    }),
    parameter: z.string({
      description: "A file path or URL to open with the application"
    }).optional()
  },
  async ({ app_name, parameter }) => {
    // Implementation for opening with parameters
  }
);

Conclusion

In this tutorial, we’ve built a practical MCP server that allows Claude Desktop to open applications on your macOS system. This server demonstrates how LLMs can interact with your operating system in a controlled, permission-based manner.

The Model Context Protocol opens up endless possibilities for AI assistants to help with your daily tasks. By extending this simple app opener, you could build more complex integrations that allow Claude to:

  • Manage your files
  • Control system settings
  • Automate repetitive tasks
  • Integrate with your favorite applications

The key advantage of MCP is the standardized way it enables these interactions, ensuring security and consistency across different applications and services.

What MCP servers would you like to build next? Share your ideas and experiments in the comments!

This article was proofread and edited with AI assistance.

Cookies