@neirth/sony-camera-mcp
Version:
MCP Server for controlling Sony Alpha 6100 camera
476 lines • 16.9 kB
JavaScript
/**
* 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