simctl-mcp
Version:
simctl mcp server
620 lines (619 loc) • 23 kB
JavaScript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import express from 'express';
import { Simctl } from 'node-simctl';
import { z } from 'zod';
const Logger = console;
export function createSimctlMcpServer() {
const server = new McpServer({
name: 'simctl-mcp-server',
version: '0.0.1',
}, {
capabilities: {
logging: {},
tools: {},
},
});
let sseTransport = null;
// Create new device
server.tool('create_device', 'Create a new simulator device', {
name: z.string().describe('Device name'),
runtime: z.string().describe('Runtime version'),
deviceType: z.string().describe('Device type'),
}, async ({ name, runtime, deviceType }) => {
try {
const simctl = new Simctl();
const device = await simctl.createDevice(name, runtime, deviceType);
return {
content: [{ type: 'text', text: JSON.stringify(device) }],
};
}
catch (error) {
Logger.error(`Failed to create device: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to create device: ${error}` }],
};
}
});
// Delete device
server.tool('delete_device', 'Delete the specified simulator device', {
udid: z.string().describe('Device unique identifier'),
}, async ({ udid }) => {
try {
const simctl = new Simctl({ udid });
await simctl.deleteDevice();
return {
content: [{ type: 'text', text: 'Device deleted successfully' }],
};
}
catch (error) {
Logger.error(`Failed to delete device: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to delete device: ${error}` }],
};
}
});
// Boot device
server.tool('boot_device', 'Boot simulator device', {
udid: z.string().describe('Device unique identifier'),
}, async ({ udid }) => {
try {
const simctl = new Simctl({ udid });
await simctl.bootDevice();
return {
content: [{ type: 'text', text: 'Device booted successfully' }],
};
}
catch (error) {
Logger.error(`Failed to boot device: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to boot device: ${error}` }],
};
}
});
// Shutdown device
server.tool('shutdown_device', 'Shutdown simulator device', {
udid: z.string().describe('Device unique identifier'),
}, async ({ udid }) => {
try {
const simctl = new Simctl({ udid });
await simctl.shutdownDevice();
return {
content: [{ type: 'text', text: 'Device shutdown successfully' }],
};
}
catch (error) {
Logger.error(`Failed to shutdown device: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to shutdown device: ${error}` }],
};
}
});
// Get environment variable
server.tool('get_env', 'Get device environment variable', {
udid: z.string().describe('Device unique identifier'),
key: z.string().describe('Environment variable key'),
},
// @ts-expect-error skip
async ({ udid, key }) => {
try {
const simctl = new Simctl({ udid });
const value = await simctl.getEnv(key);
return {
content: [{ type: 'text', text: value }],
};
}
catch (error) {
Logger.error(`Failed to get environment variable: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to get environment variable: ${error}` }],
};
}
});
// Open URL
server.tool('open_url', 'Open URL in device', {
udid: z.string().describe('Device unique identifier'),
url: z.string().describe('URL to open'),
}, async ({ udid, url }) => {
try {
const simctl = new Simctl({ udid });
await simctl.openUrl(url);
return {
content: [{ type: 'text', text: 'URL opened successfully' }],
};
}
catch (error) {
Logger.error(`Failed to open URL: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to open URL: ${error}` }],
};
}
});
// Add media
server.tool('add_media', 'Add media files to device', {
udid: z.string().describe('Device unique identifier'),
path: z.string().describe('Media file path'),
}, async ({ udid, path }) => {
try {
const simctl = new Simctl({ udid });
await simctl.addMedia(path);
return {
content: [{ type: 'text', text: 'Media files added successfully' }],
};
}
catch (error) {
Logger.error(`Failed to add media files: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to add media files: ${error}` }],
};
}
});
// Install app
server.tool('install_app', 'Install app on device', {
udid: z.string().describe('Device unique identifier'),
path: z.string().describe('App file path'),
}, async ({ udid, path }) => {
try {
const simctl = new Simctl({ udid });
await simctl.installApp(path);
return {
content: [{ type: 'text', text: 'App installed successfully' }],
};
}
catch (error) {
Logger.error(`Failed to install app: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to install app: ${error}` }],
};
}
});
// Uninstall app
server.tool('uninstall_app', 'Uninstall app from device', {
udid: z.string().describe('Device unique identifier'),
bundleId: z.string().describe('App Bundle ID'),
}, async ({ udid, bundleId }) => {
try {
const simctl = new Simctl({ udid });
await simctl.removeApp(bundleId);
return {
content: [{ type: 'text', text: 'App uninstalled successfully' }],
};
}
catch (error) {
Logger.error(`Failed to uninstall app: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to uninstall app: ${error}` }],
};
}
});
// Get app container
server.tool('get_app_container', 'Get installed app container path', {
udid: z.string().describe('Device unique identifier'),
bundleId: z.string().describe('App Bundle ID'),
}, async ({ udid, bundleId }) => {
try {
const simctl = new Simctl({ udid });
const path = await simctl.getAppContainer(bundleId);
return {
content: [{ type: 'text', text: path }],
};
}
catch (error) {
Logger.error(`Failed to get app container path: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to get app container path: ${error}` }],
};
}
});
// Launch app
server.tool('launch_app', 'Launch app on device', {
udid: z.string().describe('Device unique identifier'),
bundleId: z.string().describe('App Bundle ID'),
}, async ({ udid, bundleId }) => {
try {
const simctl = new Simctl({ udid });
await simctl.launchApp(bundleId);
return {
content: [{ type: 'text', text: 'App launched successfully' }],
};
}
catch (error) {
Logger.error(`Failed to launch app: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to launch app: ${error}` }],
};
}
});
// Terminate app
server.tool('terminate_app', 'Terminate running app on device', {
udid: z.string().describe('Device unique identifier'),
bundleId: z.string().describe('App Bundle ID'),
}, async ({ udid, bundleId }) => {
try {
const simctl = new Simctl({ udid });
await simctl.terminateApp(bundleId);
return {
content: [{ type: 'text', text: 'App terminated successfully' }],
};
}
catch (error) {
Logger.error(`Failed to terminate app: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to terminate app: ${error}` }],
};
}
});
// List devices
server.tool('list_devices', 'List available simulator devices', {}, async () => {
try {
const simctl = new Simctl();
const devices = await simctl.getDevices();
return {
content: [{ type: 'text', text: JSON.stringify(devices) }],
};
}
catch (error) {
Logger.error(`Failed to get device list: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to get device list: ${error}` }],
};
}
});
// List device types
server.tool('list_device_types', 'List available device types', {}, async () => {
try {
const simctl = new Simctl();
const deviceTypes = await simctl.getDeviceTypes();
return {
content: [{ type: 'text', text: JSON.stringify(deviceTypes) }],
};
}
catch (error) {
Logger.error(`Failed to get device types: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to get device types: ${error}` }],
};
}
});
// List runtimes
server.tool('list_runtimes', 'List available runtimes', {}, async () => {
try {
const simctl = new Simctl();
const runtimes = await simctl.list();
return {
content: [{ type: 'text', text: JSON.stringify(runtimes) }],
};
}
catch (error) {
Logger.error(`Failed to get runtimes: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to get runtimes: ${error}` }],
};
}
});
// Get app info
server.tool('get_app_info', 'Get installed app information', {
udid: z.string().describe('Device unique identifier'),
bundleId: z.string().describe('App Bundle ID'),
}, async ({ udid, bundleId }) => {
try {
const simctl = new Simctl({ udid });
const info = await simctl.appInfo(bundleId);
return {
content: [{ type: 'text', text: JSON.stringify(info) }],
};
}
catch (error) {
Logger.error(`Failed to get app info: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to get app info: ${error}` }],
};
}
});
// Get appearance
server.tool('get_appearance', 'Get device appearance settings', {
udid: z.string().describe('Device unique identifier'),
}, async ({ udid }) => {
try {
const simctl = new Simctl({ udid });
const appearance = await simctl.getAppearance();
return {
content: [{ type: 'text', text: appearance }],
};
}
catch (error) {
Logger.error(`Failed to get appearance: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to get appearance: ${error}` }],
};
}
});
// Set appearance
server.tool('set_appearance', 'Set device appearance', {
udid: z.string().describe('Device unique identifier'),
appearance: z.string().describe('Appearance setting (light/dark)'),
}, async ({ udid, appearance }) => {
try {
const simctl = new Simctl({ udid });
await simctl.setAppearance(appearance);
return {
content: [{ type: 'text', text: 'Appearance set successfully' }],
};
}
catch (error) {
Logger.error(`Failed to set appearance: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to set appearance: ${error}` }],
};
}
});
// Push notification
server.tool('push_notification', 'Send simulated push notification to device', {
udid: z.string().describe('Device unique identifier'),
bundleId: z.string().describe('App Bundle ID'),
payload: z.string().describe('Push notification JSON payload'),
}, async ({ udid, bundleId, payload }) => {
try {
const simctl = new Simctl({ udid });
// @ts-expect-error skip
await simctl.pushNotification(bundleId, JSON.parse(payload));
return {
content: [{ type: 'text', text: 'Push notification sent successfully' }],
};
}
catch (error) {
Logger.error(`Failed to send push notification: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to send push notification: ${error}` }],
};
}
});
// Grant permission
server.tool('grant_permission', 'Grant permission to app', {
udid: z.string().describe('Device unique identifier'),
bundleId: z.string().describe('App Bundle ID'),
permission: z.string().describe('Permission type'),
}, async ({ udid, bundleId, permission }) => {
try {
const simctl = new Simctl({ udid });
await simctl.grantPermission(bundleId, permission);
return {
content: [{ type: 'text', text: 'Permission granted successfully' }],
};
}
catch (error) {
Logger.error(`Failed to grant permission: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to grant permission: ${error}` }],
};
}
});
// Revoke permission
server.tool('revoke_permission', 'Revoke app permission', {
udid: z.string().describe('Device unique identifier'),
bundleId: z.string().describe('App Bundle ID'),
permission: z.string().describe('Permission type'),
}, async ({ udid, bundleId, permission }) => {
try {
const simctl = new Simctl({ udid });
await simctl.revokePermission(bundleId, permission);
return {
content: [{ type: 'text', text: 'Permission revoked successfully' }],
};
}
catch (error) {
Logger.error(`Failed to revoke permission: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to revoke permission: ${error}` }],
};
}
});
// Reset permission
server.tool('reset_permission', 'Reset all app permissions', {
udid: z.string().describe('Device unique identifier'),
bundleId: z.string().describe('App Bundle ID'),
permission: z.string().describe('Permission type'),
}, async ({ udid, bundleId, permission }) => {
try {
const simctl = new Simctl({ udid });
await simctl.resetPermission(bundleId, permission);
return {
content: [{ type: 'text', text: 'Permissions reset successfully' }],
};
}
catch (error) {
Logger.error(`Failed to reset permissions: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to reset permissions: ${error}` }],
};
}
});
// Add root certificate
server.tool('add_root_certificate', 'Add root certificate to device keychain', {
udid: z.string().describe('Device unique identifier'),
path: z.string().describe('Certificate file path'),
}, async ({ udid, path }) => {
try {
const simctl = new Simctl({ udid });
await simctl.addRootCertificate(path);
return {
content: [{ type: 'text', text: 'Root certificate added successfully' }],
};
}
catch (error) {
Logger.error(`Failed to add root certificate: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to add root certificate: ${error}` }],
};
}
});
// Add certificate
server.tool('add_certificate', 'Add certificate to device keychain', {
udid: z.string().describe('Device unique identifier'),
path: z.string().describe('Certificate file path'),
}, async ({ udid, path }) => {
try {
const simctl = new Simctl({ udid });
await simctl.addCertificate(path);
return {
content: [{ type: 'text', text: 'Certificate added successfully' }],
};
}
catch (error) {
Logger.error(`Failed to add certificate: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to add certificate: ${error}` }],
};
}
});
// Reset keychain
server.tool('reset_keychain', 'Reset device keychain', {
udid: z.string().describe('Device unique identifier'),
}, async ({ udid }) => {
try {
const simctl = new Simctl({ udid });
await simctl.resetKeychain();
return {
content: [{ type: 'text', text: 'Keychain reset successfully' }],
};
}
catch (error) {
Logger.error(`Failed to reset keychain: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to reset keychain: ${error}` }],
};
}
});
// Get screenshot
server.tool('get_screenshot', 'Get device screenshot', {
udid: z.string().describe('Device unique identifier'),
path: z.string().describe('Screenshot save path'),
}, async ({ udid, path }) => {
try {
const simctl = new Simctl({ udid });
const r = await simctl.exec('io', {
args: [udid, 'screenshot', path],
});
return {
content: [{ type: 'text', text: r.stdout || r.stderr }],
};
}
catch (error) {
Logger.error(`Failed to get screenshot: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to get screenshot: ${error}` }],
};
}
});
// List apps
server.tool('list_apps', 'List installed apps on device', {
udid: z.string().describe('Device unique identifier'),
}, async ({ udid }) => {
try {
const simctl = new Simctl({ udid });
const r = await simctl.exec('listapps', {
args: [udid],
});
return {
content: [{ type: 'text', text: r.stdout || r.stderr }],
};
}
catch (error) {
Logger.error(`Failed to list apps: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to list apps: ${error}` }],
};
}
});
// Set pasteboard content
server.tool('set_pasteboard', 'Copy content to the device pasteboard', {
udid: z.string().describe('Device unique identifier'),
content: z.string().describe('Content to copy to pasteboard'),
}, async ({ udid, content }) => {
try {
const simctl = new Simctl({ udid });
await simctl.setPasteboard(content);
return {
content: [{ type: 'text', text: 'Content copied to pasteboard successfully' }],
};
}
catch (error) {
Logger.error(`Failed to copy content to pasteboard: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to copy content to pasteboard: ${error}` }],
};
}
});
// Get pasteboard content
server.tool('get_pasteboard', 'Get the contents of the device pasteboard', {
udid: z.string().describe('Device unique identifier'),
}, async ({ udid }) => {
try {
const simctl = new Simctl({ udid });
const r = await simctl.getPasteboard();
return {
content: [{ type: 'text', text: r }],
};
}
catch (error) {
Logger.error(`Failed to get pasteboard content: ${error}`);
return {
isError: true,
content: [{ type: 'text', text: `Failed to get pasteboard content: ${error}` }],
};
}
});
return {
async connect(transport) {
await server.connect(transport);
},
async startHttpServer(port) {
const app = express();
app.get('/sse', async (req, res) => {
console.log('New SSE connection established');
sseTransport = new SSEServerTransport('/messages', res);
await server.connect(sseTransport);
});
app.post('/messages', async (req, res) => {
if (!sseTransport) {
res.sendStatus(400);
return;
}
await sseTransport.handlePostMessage(req, res);
});
app.listen(port, () => {
Logger.log(`HTTP server listening on port ${port}`);
Logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
Logger.log(`Message endpoint available at http://localhost:${port}/messages`);
});
},
};
}