UNPKG

@neirth/sony-camera-mcp

Version:

MCP Server for controlling Sony Alpha 6100 camera

476 lines 16.9 kB
#!/usr/bin/env node /** * Sony Camera MCP Server * MCP server to control Sony Alpha cameras compatible with Model Context Protocol * */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import SonyCamera from "./sony-camera.js"; import fetch from "node-fetch"; // Type for custom errors class CameraError extends Error { details; constructor(message, details) { super(message); this.details = details; this.name = "CameraError"; } } // Validation schemas using Zod const ContentSchema = z.object({ content: z.array(z.union([ z.object({ type: z.literal('text'), text: z.string() }), z.object({ type: z.literal('image'), data: z.string(), mimeType: z.string() }) ])).optional(), isError: z.boolean().optional(), _meta: z.record(z.unknown()).optional() }); // Schema definitions as direct objects (not as Zod objects) const ExposureControlSchema = { action: z.enum(["get", "set"], { errorMap: () => ({ message: "Action must be 'get' or 'set'" }), }), parameter: z .enum(["iso", "shutter_speed", "ev", "white_balance"], { errorMap: () => ({ message: "Invalid parameter", }), }) .optional(), value: z .union([ z.string(), z.number(), z.enum([ // ISO values "AUTO", "50", "100", "200", "400", "800", "1600", "3200", "6400", "12800", // Shutter speed values "BULB", "30'", "25'", "20'", "15'", "13'", "10'", "8'", "6'", "5'", "4'", "3.2'", "2.5'", "2'", "1.6'", "1.3'", "1'", "0.8", "0.6", "0.5", "0.4", "0.3", "1/4", "1/8", "1/15", "1/30", "1/60", "1/125", "1/250", "1/500", "1/1000", "1/2000", "1/4000", "1/8000", // Focus modes "AF-S", "AF-C", "DMF", "MF", // White balance modes "Auto WB", "Daylight", "Shade", "Cloudy", "Incandescent", "Fluorescent", "Flash", "Custom", ]), ]) .optional(), }; const ShootControlSchema = { action: z.enum(["capture"], { errorMap: () => ({ message: "Action must be 'capture'" }), }), }; const LiveViewSchema = { action: z.enum(["start", "stop"], { errorMap: () => ({ message: "Action must be 'start' or 'stop'" }), }), }; const ZoomControlSchema = { action: z.enum(["in", "out"], { errorMap: () => ({ message: "Action must be 'in' or 'out'" }), }), type: z.enum(["1shot", "start", "stop"], { errorMap: () => ({ message: "Type must be '1shot', 'start' or 'stop'" }), }), }; const ShootModeSchema = { action: z.enum(["get", "set"], { errorMap: () => ({ message: "Action must be 'get' or 'set'" }), }), mode: z .enum(["still", "movie"], { errorMap: () => ({ message: "Mode must be 'still' or 'movie'", }), }) .optional(), }; // Default configuration const config = { port: process.env.PORT || 3000, cameraIp: process.env.CAMERA_IP || "192.168.122.1", cameraPort: process.env.CAMERA_PORT || "10000", ddXmlPort: process.env.DD_XML_PORT || "64321", debug: process.env.DEBUG === "true" || false, }; // Create camera instance const camera = new SonyCamera({ cameraIp: config.cameraIp, cameraPort: config.cameraPort, ddXmlPort: config.ddXmlPort, debug: config.debug, }); // Tools are registered directly using server.tool(name, schema, handler) // Each handler receives destructured parameters and must return an object with the response // Prompts are registered with server.prompt() const asString = (value) => String(value); // Function to initialize camera connection async function init() { console.error("🔄 Initializing camera connection..."); await camera.connect(); console.error("✅ Camera connected successfully"); // Verify we can get basic information await camera.getApplicationInfo(); console.error("✅ Camera communication verified"); return true; } // Function to initialize and run the server async function runServer() { // Create MCP server with the new McpServer class const server = new McpServer({ name: "Sony Alpha Camera Control", version: "1.0.0", }, { capabilities: { tools: {}, }, }); try { await init(); // Register tools // Each tool has a name, a Zod schema for parameter validation, an output schema and a handler that implements the logic server.tool("exposure_control", "Exposure Control", ExposureControlSchema, // We no longer use .shape because it's now a flat object async (params) => { try { const { action, parameter, value } = params; if (!action) { throw new CameraError("Action is required"); } if (action === "get") { if (!parameter) { const settings = { iso: await camera.getIsoSpeedRate(), shutter_speed: await camera.getShutterSpeed(), ev: await camera.getExposureCompensation(), }; return { content: [{ type: "text", text: `Current exposure settings: ISO: ${settings.iso}, Shutter speed: ${settings.shutter_speed}, Exposure compensation: ${settings.ev}`, }, ], }; } let result; switch (parameter) { case "iso": result = await camera.getIsoSpeedRate(); break; case "shutter_speed": result = await camera.getShutterSpeed(); break; case "ev": result = await camera.getExposureCompensation(); break; default: throw new CameraError(`Invalid parameter: ${parameter}`); } return { content: [ { type: "text", text: `Current ${parameter} value: ${result}`, }, ], }; } else if (action === "set") { if (!parameter || !value) { throw new CameraError("For 'set' action, both parameter and value must be specified"); } switch (parameter) { case "iso": await camera.setIsoSpeedRate(asString(value)); break; case "shutter_speed": await camera.setShutterSpeed(asString(value)); break; case "ev": const numValue = typeof value === "string" ? parseFloat(value) : value; await camera.setExposureCompensation(numValue); break; default: throw new CameraError(`Invalid parameter: ${parameter}`); } return { content: [ { type: "text", text: `${parameter} set to ${value}`, }, ], }; } throw new CameraError("Invalid action"); } catch (error) { return { content: [ { type: "text", text: `Error controlling exposure: ${error}`, }, ], isError: true, }; } }); server.tool("shoot_control", "Shoot Control", ShootControlSchema, // We no longer use .shape because it's now a flat object async (params) => { try { if (!params.action) { throw new CameraError("Action is required"); } const result = await camera.takePicture(); if (result) { try { const response = await fetch(result); if (!response.ok) { throw new Error(`Error getting image: ${response.status} ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); const base64Image = Buffer.from(arrayBuffer).toString("base64"); return { content: [ { type: "image", data: base64Image, mimeType: "image/jpeg" }, ], }; } catch (error) { console.error(`Error processing image: ${error}`); // In case of error processing the image, return the URL but without the image return { content: [ { type: "text", text: `Picture captured. URL: ${result}\nError processing image: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } else { // If there are no results or they don't have the expected format throw new CameraError("Unexpected response format when taking picture"); } } catch (error) { console.error(`Error taking picture: ${error}`); return { content: [ { type: "text", text: `Error taking picture: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } }); server.tool("zoom_control", "Zoom Control", ZoomControlSchema, // We no longer use .shape because it's now a flat object async (params) => { try { const { action, type } = params; if (!action) { throw new CameraError("Action is required"); } await camera.zoom(action, type); return { content: [ { type: "text", text: `Zoom ${action} (${type})`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error controlling zoom: ${error}`, }, ], isError: true, }; } }); server.tool("shoot_mode", "Shoot Mode", ShootModeSchema, // We no longer use .shape because it's now a flat object async (params) => { try { const { action, mode } = params; if (!action) { throw new CameraError("Action is required"); } if (action === "get") { const [currentMode] = await camera.getShootMode(); return { content: [ { type: "text", text: `Current mode: ${currentMode}`, }, ], }; } else if (action === "set" && mode) { await camera.setShootMode(mode); return { content: [ { type: "text", text: `Mode set to ${mode}`, }, ], }; } throw new CameraError("Invalid parameters"); } catch (error) { return { content: [ { type: "text", text: `Error changing shoot mode: ${error}`, }, ], isError: true, }; } }); server.tool("live_view", "Live View", LiveViewSchema, // We no longer use .shape because it's now a flat object async (params) => { try { const { action } = params; if (!action) { throw new CameraError("Action is required"); } if (action === "start") { const result = await camera.startLiveview(); if (Array.isArray(result) && result[0]) { return { content: [ { type: "text", text: "Live view started", }, { type: "text", text: `Live view URL: ${result[0]}`, }, ], }; } throw new CameraError("Could not start live view"); } else { await camera.stopLiveview(); return { content: [ { type: "text", text: "Live view stopped", }, ], }; } } catch (error) { return { content: [ { type: "text", text: `Error stopping live view: ${error}`, }, ], isError: true, }; } }); server.prompt("cinematic_setup", {}, () => ({ messages: [ { role: "user", content: { type: "text", text: "As a cinematographer, I need you to evaluate the scene and configure an optimal cinematic look, documenting the changes visually.", }, }, ], })); } catch (error) { console.error("❌ Error registering tools:", error); console.error("❌ Tools not registered, but server remains running"); } // Connect with STDIO transport const transport = new StdioServerTransport(); await server.connect(transport); console.error("✅ MCP Server started in STDIO mode"); } // Run the server runServer().catch((error) => { console.error("❌ Fatal error starting server:", error); process.exit(1); }); //# sourceMappingURL=index.js.map