Skip to main content
Glama

Seam MCP Server

by keithah
index.ts17.5 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { Seam } from "seam"; // Configuration schema - automatically detected by Smithery export const configSchema = z.object({ seamApiKey: z.string().describe("Your Seam API key from https://console.seam.co/"), }); export default function createServer({ config }: { config: z.infer<typeof configSchema> }) { console.log('[Server] Initializing...'); console.log('[Server] Config received, keys:', Object.keys(config)); // Create MCP server const server = new McpServer({ name: "seam-mcp-server", title: "Seam Smart Lock Control", version: "0.1.0" }); // Lazy initialization of Seam client - only create when needed let seamClient: Seam | null = null; const getSeamClient = () => { console.log('[SeamClient] Initializing...'); if (!seamClient) { if (!config.seamApiKey) { console.error('[SeamClient] ERROR: No API key'); throw new Error("Seam API key not configured. Please set seamApiKey in your MCP server configuration."); } console.log('[SeamClient] API key found, length:', config.seamApiKey.length); try { seamClient = new Seam(config.seamApiKey); console.log('[SeamClient] ✓ Client created'); } catch (err) { console.error('[SeamClient] ERROR creating:', err); throw err; } } return seamClient; }; // Register list_locks tool server.tool( "list_locks", "List all smart locks connected to your Seam account", {}, async () => { try { console.log('[list_locks] Starting...'); const seamClient = getSeamClient(); console.log('[list_locks] Seam client initialized'); const locks = await seamClient.locks.list(); console.log(`[list_locks] Found ${locks.length} locks`); return { total_locks: locks.length, locks: locks.map(lock => ({ device_id: lock.device_id, name: lock.properties?.name || lock.device_id, manufacturer: lock.properties?.manufacturer, model: lock.properties?.model, locked: lock.properties?.locked, battery_level: lock.properties?.battery_level, online: lock.properties?.online, })) }; } catch (error) { console.error('[list_locks] Error:', error); const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : ''; throw new Error(`Failed to list locks: ${errorMessage}\nStack: ${errorStack}`); } } ); // Register get_status tool server.tool( "get_status", "Get a comprehensive status overview of all locks including battery levels, lock states, and any issues", {}, async () => { try { console.log('[get_status] Getting status overview...'); const locks = await getSeamClient().locks.list(); // Categorize locks const locked = locks.filter(l => l.properties?.locked === true); const unlocked = locks.filter(l => l.properties?.locked === false); const offline = locks.filter(l => l.properties?.online === false); const online = locks.filter(l => l.properties?.online === true); // Battery analysis const lowBattery = locks.filter(l => l.properties?.battery_level !== undefined && l.properties.battery_level < 0.3 ); const mediumBattery = locks.filter(l => l.properties?.battery_level !== undefined && l.properties.battery_level >= 0.3 && l.properties.battery_level < 0.6 ); // Issues const locksWithErrors = locks.filter(l => l.errors && l.errors.length > 0); const locksWithWarnings = locks.filter(l => l.warnings && l.warnings.length > 0); // Build status summary const summary = { total_locks: locks.length, lock_states: { locked: locked.length, unlocked: unlocked.length, locked_percentage: locks.length > 0 ? Math.round((locked.length / locks.length) * 100) : 0, }, connectivity: { online: online.length, offline: offline.length, offline_locks: offline.map(l => ({ name: l.properties?.name || l.device_id, device_id: l.device_id, })), }, battery_status: { low_battery_count: lowBattery.length, medium_battery_count: mediumBattery.length, low_battery_locks: lowBattery.map(l => ({ name: l.properties?.name || l.device_id, battery_level: Math.round((l.properties?.battery_level || 0) * 100) + '%', device_id: l.device_id, })), medium_battery_locks: mediumBattery.map(l => ({ name: l.properties?.name || l.device_id, battery_level: Math.round((l.properties?.battery_level || 0) * 100) + '%', device_id: l.device_id, })), }, issues: { errors_count: locksWithErrors.length, warnings_count: locksWithWarnings.length, locks_with_errors: locksWithErrors.map(l => ({ name: l.properties?.name || l.device_id, errors: l.errors, device_id: l.device_id, })), locks_with_warnings: locksWithWarnings.map(l => ({ name: l.properties?.name || l.device_id, warnings: l.warnings, device_id: l.device_id, })), }, all_locks_summary: locks.map(lock => ({ name: lock.properties?.name || lock.device_id, status: lock.properties?.locked ? '🔒 Locked' : '🔓 Unlocked', online: lock.properties?.online ? '✅ Online' : '❌ Offline', battery: lock.properties?.battery_level !== undefined ? Math.round(lock.properties.battery_level * 100) + '%' : 'N/A', device_id: lock.device_id, })), }; console.log(`[get_status] Status: ${locked.length}/${locks.length} locked, ${offline.length} offline, ${lowBattery.length} low battery`); return summary; } catch (error) { console.error('[get_status] Error:', error); throw new Error(`Failed to get status: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register get_lock tool server.tool( "get_lock", "Get detailed information about a specific lock", { device_id: z.string().describe("The device ID of the lock"), }, async ({ device_id }) => { try { const lock = await getSeamClient().locks.get({ device_id }); return { device_id: lock.device_id, name: lock.properties?.name || lock.device_id, manufacturer: lock.properties?.manufacturer, model: lock.properties?.model, locked: lock.properties?.locked, battery_level: lock.properties?.battery_level, online: lock.properties?.online, location: lock.location, created_at: lock.created_at, capabilities: lock.capabilities, errors: lock.errors, warnings: lock.warnings, }; } catch (error) { throw new Error(`Failed to get lock details: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register lock_door tool server.tool( "lock_door", "Lock a specific door", { device_id: z.string().describe("The device ID of the lock to lock"), }, async ({ device_id }) => { try { const result = await getSeamClient().locks.lockDoor({ device_id }); return { status: "success", message: `Successfully locked door ${device_id}`, action_attempt: result, }; } catch (error) { throw new Error(`Failed to lock door: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register unlock_door tool server.tool( "unlock_door", "Unlock a specific door", { device_id: z.string().describe("The device ID of the lock to unlock"), }, async ({ device_id }) => { try { const result = await getSeamClient().locks.unlockDoor({ device_id }); return { status: "success", message: `Successfully unlocked door ${device_id}`, action_attempt: result, }; } catch (error) { throw new Error(`Failed to unlock door: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register get_lock_status tool server.tool( "get_lock_status", "Get the current lock status (locked/unlocked) of a specific lock", { device_id: z.string().describe("The device ID of the lock"), }, async ({ device_id }) => { try { const lock = await getSeamClient().locks.get({ device_id }); const isLocked = lock.properties?.locked; return { device_id: lock.device_id, name: lock.properties?.name || lock.device_id, locked: isLocked, status: isLocked ? "locked" : "unlocked", status_emoji: isLocked ? "🔒" : "🔓", battery_level: lock.properties?.battery_level, online: lock.properties?.online, }; } catch (error) { throw new Error(`Failed to get lock status: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register create_access_code tool server.tool( "create_access_code", "Create an access code on a specific lock with optional time limits", { device_id: z.string().describe("The device ID of the lock"), code: z.string().optional().describe("The PIN code (e.g., '1234'). If not provided, a random code will be generated"), name: z.string().describe("Name/label for the access code (e.g., 'Guest Code', 'Cleaner')"), starts_at: z.string().optional().describe("When the code becomes active (ISO 8601 format, e.g., '2025-01-01T16:00:00Z')"), ends_at: z.string().optional().describe("When the code expires (ISO 8601 format, e.g., '2025-01-22T12:00:00Z')"), }, async ({ device_id, code, name, starts_at, ends_at }) => { try { const lock = await getSeamClient().locks.get({ device_id }); if (!lock.can_program_online_access_codes) { throw new Error(`Lock ${lock.properties?.name || device_id} does not support online access codes`); } const params: any = { device_id, name, }; if (code) params.code = code; if (starts_at) params.starts_at = starts_at; if (ends_at) params.ends_at = ends_at; const result = await getSeamClient().accessCodes.create(params); return { status: "success", message: `Created access code '${name}' on ${lock.properties?.name || device_id}`, access_code: { access_code_id: result.access_code_id, code: result.code, name: result.name, device_id: result.device_id, starts_at: result.starts_at, ends_at: result.ends_at, status: result.status, }, }; } catch (error) { throw new Error(`Failed to create access code: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register create_access_code_on_multiple_locks tool server.tool( "create_access_code_on_multiple_locks", "Create the same access code on multiple locks (useful for creating one code for all locks in a location)", { device_ids: z.array(z.string()).describe("Array of device IDs to create the access code on"), code: z.string().optional().describe("The PIN code (e.g., '1234'). If not provided, random codes will be generated"), name: z.string().describe("Name/label for the access code (e.g., 'Guest Code', 'Seattle Locks')"), starts_at: z.string().optional().describe("When the code becomes active (ISO 8601 format)"), ends_at: z.string().optional().describe("When the code expires (ISO 8601 format)"), location_filter: z.string().optional().describe("Optional: filter locks by location name (e.g., 'Seattle', 'Building A')"), }, async ({ device_ids, code, name, starts_at, ends_at, location_filter }) => { try { let targetDeviceIds = device_ids; // If location filter is provided, filter devices by location if (location_filter) { const allLocks = await getSeamClient().locks.list(); const filteredLocks = allLocks.filter(lock => { const location = lock.location?.location_name || lock.location?.timezone || ''; const lockName = lock.properties?.name || ''; const searchTerm = location_filter.toLowerCase(); return location.toLowerCase().includes(searchTerm) || lockName.toLowerCase().includes(searchTerm); }); if (filteredLocks.length === 0) { throw new Error(`No locks found matching location filter: '${location_filter}'`); } targetDeviceIds = filteredLocks.map(lock => lock.device_id); } if (targetDeviceIds.length === 0) { throw new Error('No device IDs provided'); } const params: any = { device_ids: targetDeviceIds, name, }; if (code) params.code = code; if (starts_at) params.starts_at = starts_at; if (ends_at) params.ends_at = ends_at; const result = await getSeamClient().accessCodes.createMultiple(params); return { status: "success", message: `Created access code '${name}' on ${targetDeviceIds.length} lock(s)`, access_codes: result.access_codes.map((ac: any) => ({ access_code_id: ac.access_code_id, code: ac.code, name: ac.name, device_id: ac.device_id, starts_at: ac.starts_at, ends_at: ac.ends_at, status: ac.status, })), total_codes_created: result.access_codes.length, }; } catch (error) { throw new Error(`Failed to create access codes: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register list_access_codes tool server.tool( "list_access_codes", "List all access codes, optionally filtered by device", { device_id: z.string().optional().describe("Optional: filter by specific device ID"), }, async ({ device_id }) => { try { const params: any = {}; if (device_id) params.device_id = device_id; const codes = await getSeamClient().accessCodes.list(params); return { total_codes: codes.length, access_codes: codes.map((code: any) => ({ access_code_id: code.access_code_id, device_id: code.device_id, name: code.name, code: code.code, starts_at: code.starts_at, ends_at: code.ends_at, status: code.status, type: code.type, })), }; } catch (error) { throw new Error(`Failed to list access codes: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register delete_access_code tool server.tool( "delete_access_code", "Delete an access code from a lock", { access_code_id: z.string().describe("The ID of the access code to delete"), }, async ({ access_code_id }) => { try { await getSeamClient().accessCodes.delete({ access_code_id }); return { status: "success", message: `Successfully deleted access code ${access_code_id}`, }; } catch (error) { throw new Error(`Failed to delete access code: ${error instanceof Error ? error.message : String(error)}`); } } ); // Register update_access_code tool server.tool( "update_access_code", "Update an existing access code (change name, code, or time limits)", { access_code_id: z.string().describe("The ID of the access code to update"), name: z.string().optional().describe("New name for the access code"), code: z.string().optional().describe("New PIN code"), starts_at: z.string().optional().describe("New start time (ISO 8601 format)"), ends_at: z.string().optional().describe("New end time (ISO 8601 format)"), }, async ({ access_code_id, name, code, starts_at, ends_at }) => { try { const params: any = { access_code_id }; if (name) params.name = name; if (code) params.code = code; if (starts_at) params.starts_at = starts_at; if (ends_at) params.ends_at = ends_at; await getSeamClient().accessCodes.update(params); return { status: "success", message: `Successfully updated access code ${access_code_id}`, }; } catch (error) { throw new Error(`Failed to update access code: ${error instanceof Error ? error.message : String(error)}`); } } ); // Return the server object (Smithery CLI handles transport) return server.server; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keithah/seam-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server