Skip to main content
Glama
management-api-server.ts25.1 kB
#!/usr/bin/env node import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import dotenv from 'dotenv'; import { ManagementApiClient, handleApiError } from './utils/http.js'; import { validateManagementApiAuth } from './utils/auth.js'; import { managementApiLogger } from './utils/logger.js'; // Load environment variables dotenv.config(); const MANAGEMENT_API_URL = process.env.MANAGEMENT_API_URL || 'https://saas.licensespring.com'; const MANAGEMENT_API_KEY = process.env.MANAGEMENT_API_KEY; // Validate configuration with better error handling try { validateManagementApiAuth(MANAGEMENT_API_KEY); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error('❌ Configuration Error:', errorMessage); console.error(''); console.error('Please check your environment variables:'); console.error('1. Copy .env.example to .env'); console.error('2. Set MANAGEMENT_API_KEY to your LicenseSpring Management API key'); console.error(''); console.error('For more information, see the README.md file.'); process.exit(1); } // Create HTTP client const apiClient = new ManagementApiClient(MANAGEMENT_API_URL, MANAGEMENT_API_KEY || ''); // Create MCP server const server = new McpServer({ name: 'licensespring-management-api', version: '2.0.0', }); // Track server state for health checks let serverStartTime = Date.now(); let isHealthy = true; let transport: StdioServerTransport | null = null; // Health Check Resource server.registerResource( 'health', 'licensespring://health', { title: 'Health Check', description: 'Service health status and metrics', mimeType: 'application/json' }, async (uri) => { const uptime = Date.now() - serverStartTime; const memoryUsage = process.memoryUsage(); const healthData = { status: isHealthy ? 'healthy' : 'unhealthy', service: 'licensespring-management-api', version: '2.0.0', uptime: Math.floor(uptime / 1000), timestamp: new Date().toISOString(), environment: { node_version: process.version, platform: process.platform, api_url: MANAGEMENT_API_URL, has_api_key: !!MANAGEMENT_API_KEY }, memory: { rss: Math.round(memoryUsage.rss / 1024 / 1024), heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024), heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024) } }; return { contents: [{ uri: uri.toString(), text: JSON.stringify(healthData, null, 2), mimeType: 'application/json' }] }; } ); // Resources - Expose management data server.registerResource( 'licenses-list', 'licensespring://management/licenses', { title: 'Licenses List', description: 'List of all licenses in the system', mimeType: 'application/json' }, async (uri) => { try { const response = await apiClient.get('/api/v1/licenses/?limit=100'); return { contents: [{ uri: uri.toString(), text: JSON.stringify(response.data, null, 2), mimeType: 'application/json' }] }; } catch (error) { throw new Error(`Failed to get licenses list: ${handleApiError(error)}`); } } ); server.registerResource( 'customers-list', 'licensespring://management/customers', { title: 'Customers List', description: 'List of all customers in the system', mimeType: 'application/json' }, async (uri) => { try { const response = await apiClient.get('/api/v1/customers/?limit=100'); return { contents: [{ uri: uri.toString(), text: JSON.stringify(response.data, null, 2), mimeType: 'application/json' }] }; } catch (error) { throw new Error(`Failed to get customers list: ${handleApiError(error)}`); } } ); server.registerResource( 'products-list', 'licensespring://management/products', { title: 'Products List', description: 'List of all products in the system', mimeType: 'application/json' }, async (uri) => { try { const response = await apiClient.get('/api/v1/products/?limit=100'); return { contents: [{ uri: uri.toString(), text: JSON.stringify(response.data, null, 2), mimeType: 'application/json' }] }; } catch (error) { throw new Error(`Failed to get products list: ${handleApiError(error)}`); } } ); // Prompts - Management workflows server.registerPrompt( 'license-management-workflow', { title: 'License Management Workflow', description: 'Complete workflow for managing licenses', argsSchema: { action: z.enum(['create', 'update', 'audit', 'cleanup']), customer_email: z.string().email().optional(), product_id: z.string().optional(), notes: z.string().optional() } }, ({ action, customer_email, product_id, notes }) => ({ messages: [{ role: 'user', content: { type: 'text', text: `Please help with license management workflow: Action: ${action} ${customer_email ? `Customer: ${customer_email}` : ''} ${product_id ? `Product ID: ${product_id}` : ''} ${notes ? `Notes: ${notes}` : ''} Based on the action requested: **Create**: Set up new licenses for customers 1. Verify customer exists or create new customer 2. Create license with appropriate settings 3. Configure validity period and features 4. Send activation instructions **Update**: Modify existing licenses 1. Find and review current license settings 2. Update as needed (enable/disable, extend validity) 3. Document changes made **Audit**: Review license usage and compliance 1. List all licenses and their status 2. Check for expired or unused licenses 3. Identify any compliance issues 4. Generate summary report **Cleanup**: Remove or deactivate unused licenses 1. Identify inactive or expired licenses 2. Safely deactivate or remove as appropriate 3. Update customer records 4. Document cleanup actions Use the available management tools to complete the requested workflow.` } }] }) ); server.registerPrompt( 'customer-analysis', { title: 'Customer Analysis', description: 'Analyze customer usage and license patterns', argsSchema: { customer_id: z.string().optional(), customer_email: z.string().email().optional(), analysis_type: z.enum(['usage', 'compliance', 'renewal']).optional() } }, ({ customer_id, customer_email, analysis_type = 'usage' }) => ({ messages: [{ role: 'user', content: { type: 'text', text: `Please analyze customer data: ${customer_id ? `Customer ID: ${customer_id}` : ''} ${customer_email ? `Customer Email: ${customer_email}` : ''} Analysis Type: ${analysis_type} Please perform the following analysis: **Usage Analysis**: 1. Get customer details and license history 2. Review license activation and usage patterns 3. Identify most/least used products 4. Suggest optimization opportunities **Compliance Analysis**: 1. Check all customer licenses for compliance 2. Verify license terms are being followed 3. Identify any violations or concerns 4. Recommend corrective actions **Renewal Analysis**: 1. Review upcoming license expirations 2. Analyze historical renewal patterns 3. Identify renewal opportunities and risks 4. Suggest proactive renewal strategies Use the management API tools to gather data and provide comprehensive insights.` } }] }) ); // License Management Tools server.registerTool('list_licenses', { title: 'List Licenses', description: 'List licenses with optional filtering', inputSchema: { limit: z.number().optional().default(100), offset: z.number().optional().default(0), order_by: z.string().optional(), license_key: z.string().optional(), customer_email: z.string().optional(), product_id: z.number().optional(), enabled: z.boolean().optional(), }, }, async ({ limit, offset, order_by, license_key, customer_email, product_id, enabled }) => { try { const queryParams = new URLSearchParams(); if (limit) queryParams.append('limit', limit.toString()); if (offset) queryParams.append('offset', offset.toString()); if (order_by) queryParams.append('order_by', order_by); if (license_key) queryParams.append('license_key', license_key); if (customer_email) queryParams.append('customer_email', customer_email); if (product_id) queryParams.append('product_id', product_id.toString()); if (enabled !== undefined) queryParams.append('enabled', enabled.toString()); const response = await apiClient.get(`/api/v1/licenses/?${queryParams}`); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error listing licenses: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('create_license', { title: 'Create License', description: 'Create a new license', inputSchema: { product: z.number().min(1, 'Product ID is required'), customer: z.number().min(1, 'Customer ID is required'), license_key: z.string().optional(), validity_period: z.number().optional(), enabled: z.boolean().optional().default(true), note: z.string().optional(), }, }, async ({ product, customer, license_key, validity_period, enabled, note }) => { try { const response = await apiClient.post('/api/v1/licenses/', { product, customer, license_key, validity_period, enabled, note, }); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error creating license: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('update_license', { title: 'Update License', description: 'Update an existing license', inputSchema: { id: z.number().min(1, 'License ID is required'), enabled: z.boolean().optional(), note: z.string().optional(), validity_period: z.number().optional(), }, }, async ({ id, enabled, note, validity_period }) => { try { const updateData: Record<string, unknown> = {}; if (enabled !== undefined) updateData.enabled = enabled; if (note !== undefined) updateData.note = note; if (validity_period !== undefined) updateData.validity_period = validity_period; const response = await apiClient.patch(`/api/v1/licenses/${id}/`, updateData); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error updating license: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('get_license', { title: 'Get License', description: 'Get details of a specific license', inputSchema: { id: z.number().min(1, 'License ID is required'), }, }, async ({ id }) => { try { const response = await apiClient.get(`/api/v1/licenses/${id}/`); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error getting license: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('delete_license', { title: 'Delete License', description: 'Delete a license', inputSchema: { id: z.number().min(1, 'License ID is required'), }, }, async ({ id }) => { try { await apiClient.delete(`/api/v1/licenses/${id}/`); return { content: [{ type: 'text', text: `License ${id} deleted successfully`, }], }; } catch (error) { return { content: [{ type: 'text', text: `Error deleting license: ${handleApiError(error)}`, }], isError: true, }; } }); // Customer Management Tools server.registerTool('list_customers', { title: 'List Customers', description: 'List customers with optional filtering', inputSchema: { limit: z.number().optional().default(100), offset: z.number().optional().default(0), email: z.string().optional(), company_name: z.string().optional(), }, }, async ({ limit, offset, email, company_name }) => { try { const queryParams = new URLSearchParams(); if (limit) queryParams.append('limit', limit.toString()); if (offset) queryParams.append('offset', offset.toString()); if (email) queryParams.append('email', email); if (company_name) queryParams.append('company_name', company_name); const response = await apiClient.get(`/api/v1/customers/?${queryParams}`); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error listing customers: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('create_customer', { title: 'Create Customer', description: 'Create a new customer', inputSchema: { email: z.string().email('Valid email is required'), first_name: z.string().optional(), last_name: z.string().optional(), company_name: z.string().optional(), phone: z.string().optional(), reference: z.string().optional(), }, }, async ({ email, first_name, last_name, company_name, phone, reference }) => { try { const response = await apiClient.post('/api/v1/customers/', { email, first_name, last_name, company_name, phone, reference, }); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error creating customer: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('delete_customer', { title: 'Delete Customer', description: 'Delete a customer', inputSchema: { id: z.number().min(1, 'Customer ID is required'), }, }, async ({ id }) => { try { await apiClient.delete(`/api/v1/customers/${id}/`); return { content: [{ type: 'text', text: `Customer ${id} deleted successfully`, }], }; } catch (error) { return { content: [{ type: 'text', text: `Error deleting customer: ${handleApiError(error)}`, }], isError: true, }; } }); // License User Management Tools server.registerTool('list_license_users', { title: 'List License Users', description: 'List license users with optional filtering', inputSchema: { limit: z.number().min(1).max(1000).optional().default(100), offset: z.number().min(0).optional().default(0), license_id: z.number().min(1).optional(), email: z.string().email().optional(), }, }, async ({ limit, offset, license_id, email }) => { try { const queryParams = new URLSearchParams({ limit: limit.toString(), offset: offset.toString(), }); if (license_id) { queryParams.append('license_id', license_id.toString()); } if (email) { queryParams.append('email', email); } const response = await apiClient.get(`/api/v1/license-users/?${queryParams}`); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error listing license users: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('assign_user_to_license', { title: 'Assign User to License', description: 'Assign a user to a specific license', inputSchema: { license_id: z.number().min(1, 'License ID is required'), email: z.string().email('Valid email is required'), first_name: z.string().optional(), last_name: z.string().optional(), phone_number: z.string().optional(), is_manager: z.boolean().optional().default(false), password: z.string().optional(), max_activations: z.number().min(0).optional(), total_activations: z.number().min(0).optional(), }, }, async ({ license_id, email, first_name, last_name, phone_number, is_manager, password, max_activations, total_activations }) => { try { const requestData: Record<string, unknown> = { email, is_manager, }; if (first_name) requestData.first_name = first_name; if (last_name) requestData.last_name = last_name; if (phone_number) requestData.phone_number = phone_number; if (password) requestData.password = password; if (max_activations !== undefined) requestData.max_activations = max_activations; if (total_activations !== undefined) requestData.total_activations = total_activations; const response = await apiClient.post(`/api/v1/licenses/${license_id}/assign_user/`, requestData); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error assigning user to license: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('unassign_user_from_license', { title: 'Unassign User from License', description: 'Remove a user assignment from a specific license', inputSchema: { license_id: z.number().min(1, 'License ID is required'), license_user_id: z.number().min(1, 'License user ID is required'), }, }, async ({ license_id, license_user_id }) => { try { const requestData = { license_user_id, }; const response = await apiClient.post(`/api/v1/licenses/${license_id}/unassign_user/`, requestData); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error unassigning user from license: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('set_user_activations', { title: 'Set User Activations', description: 'Set activation limits for users on a specific license', inputSchema: { license_id: z.number().min(1, 'License ID is required'), user_activations: z.record(z.string(), z.object({ max_activations: z.number().min(0).optional(), reset_total_activations: z.boolean().optional(), })).refine(obj => Object.keys(obj).length > 0, 'At least one user activation must be specified'), }, }, async ({ license_id, user_activations }) => { try { const requestData = { user_activations, }; const response = await apiClient.post(`/api/v1/licenses/${license_id}/set_users_activations/`, requestData); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error setting user activations: ${handleApiError(error)}`, }], isError: true, }; } }); // Bulk Operations Tools server.registerTool('bulk_update_licenses', { title: 'Bulk Update Licenses', description: 'Update multiple licenses in a single operation', inputSchema: { licenses: z.array(z.object({ id: z.number().min(1, 'License ID is required'), is_trial: z.boolean().optional(), enable_maintenance_period: z.boolean().optional(), enabled: z.boolean().optional(), note: z.string().optional(), validity_period: z.number().min(0).optional(), })).min(1, 'At least one license must be specified').max(100, 'Maximum 100 licenses can be updated at once'), }, }, async ({ licenses }) => { try { const requestData = { licenses, }; const response = await apiClient.post('/api/v1/licenses/bulk_update/', requestData); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error bulk updating licenses: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('bulk_disable_licenses', { title: 'Bulk Disable Licenses', description: 'Disable multiple licenses in a single operation', inputSchema: { license_ids: z.array(z.number().min(1, 'License ID must be a positive number')) .min(1, 'At least one license ID must be specified') .max(100, 'Maximum 100 licenses can be disabled at once'), }, }, async ({ license_ids }) => { try { const requestData = { license_ids, }; const response = await apiClient.post('/api/v1/licenses/disable_bulk/', requestData); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error bulk disabling licenses: ${handleApiError(error)}`, }], isError: true, }; } }); server.registerTool('import_licenses_from_csv', { title: 'Import Licenses from CSV', description: 'Import multiple licenses from a CSV file', inputSchema: { csv_file: z.string().min(1, 'CSV file content is required (base64 encoded or file path)'), product_id: z.number().min(1).optional(), customer_id: z.number().min(1).optional(), }, }, async ({ csv_file, product_id, customer_id }) => { try { const requestData: Record<string, unknown> = { csv_file, }; if (product_id) requestData.product_id = product_id; if (customer_id) requestData.customer_id = customer_id; const response = await apiClient.post('/api/v1/licenses/import_from_csv/', requestData); return { content: [{ type: 'text', text: JSON.stringify(response.data, null, 2), }], }; } catch (error) { return { content: [{ type: 'text', text: `Error importing licenses from CSV: ${handleApiError(error)}`, }], isError: true, }; } }); // Graceful shutdown handler async function gracefulShutdown(signal: string) { managementApiLogger.info(`Received ${signal}, starting graceful shutdown...`); isHealthy = false; try { // Give ongoing requests 5 seconds to complete setTimeout(() => { managementApiLogger.warn('Forcing shutdown after timeout'); process.exit(1); }, 5000); // Close the transport connection if (transport) { managementApiLogger.info('Closing server transport...'); await transport.close(); } managementApiLogger.info('Graceful shutdown completed'); process.exit(0); } catch (error) { managementApiLogger.error('Error during graceful shutdown', error); process.exit(1); } } // Register shutdown handlers process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGHUP', () => gracefulShutdown('SIGHUP')); // Handle uncaught errors process.on('uncaughtException', (error) => { managementApiLogger.error('Uncaught exception', error); isHealthy = false; gracefulShutdown('uncaughtException'); }); process.on('unhandledRejection', (reason, promise) => { managementApiLogger.error('Unhandled rejection', reason, { promise }); isHealthy = false; gracefulShutdown('unhandledRejection'); }); // Start server async function main() { try { transport = new StdioServerTransport(); await server.connect(transport); serverStartTime = Date.now(); isHealthy = true; managementApiLogger.info('LicenseSpring Management API MCP server v2.0.0 started', { api_url: MANAGEMENT_API_URL, has_api_key: !!MANAGEMENT_API_KEY }); console.error('LicenseSpring Management API MCP server v2.0.0 running on stdio'); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error('❌ Failed to start MCP server:', errorMessage); console.error(''); console.error('This could be due to:'); console.error('- Invalid MCP transport configuration'); console.error('- Port already in use'); console.error('- Permission issues'); console.error(''); console.error('Please check the server configuration and try again.'); process.exit(1); } } main().catch((error) => { console.error('❌ Unexpected server error:', error.message); console.error('Stack trace:', error.stack); process.exit(1); });

Latest Blog Posts

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/stier1ba/licensespring-mcp'

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