UNPKG

jobnimbus-mcp-server

Version:
686 lines (683 loc) 30.5 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 axios from "axios"; import * as dotenv from 'dotenv'; // Load environment variables from .env file dotenv.config(); console.error("MCP Server Script Started."); // --- Configuration --- const jobnimbusApiBase = "https://app.jobnimbus.com/api1"; // IMPORTANT: Store API key securely, e.g., environment variable const apiKey = process.env.JOBNIMBUS_API_KEY; console.error(`Read API Key: ${apiKey ? 'Yes (masked)' : 'No'}`); if (!apiKey) { console.error("FATAL ERROR: JOBNIMBUS_API_KEY environment variable is not set."); console.error("Please set this variable in your .env file or environment."); console.error("Example: export JOBNIMBUS_API_KEY=your_api_key_here"); process.exit(1); // Exit if the API key is missing } console.error("Initializing McpServer..."); // --- MCP Server Setup --- const server = new McpServer({ name: "JobNimbus MCP Server", version: "1.0.0", }); console.error("McpServer Initialized."); // --- Helper Function for API Calls --- async function callJobNimbusApi(method, endpoint, params, // For GET requests query params data // For POST/PUT request body ) { try { const response = await axios({ method, url: `${jobnimbusApiBase}${endpoint}`, headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json', // Assume JSON for POST/PUT }, params: method === 'get' ? params : undefined, data: (method === 'post' || method === 'put') ? data : undefined, timeout: 15000, // 15 second timeout }); return response.data; } catch (error) { const axiosError = error; console.error(`Error calling JobNimbus API (${method.toUpperCase()} ${endpoint}):`, axiosError.response?.status, axiosError.response?.data || axiosError.message); // Re-throw a more structured error or the original data if available throw axiosError.response?.data || new Error(`JobNimbus API request failed: ${axiosError.message}`); } } // --- Tool Implementation Helper --- function createTool(name, inputSchema, apiMethod, endpointTemplate, // Can be a template string or a function to generate the endpoint inputTransformer // Optional function to transform input before sending to API ) { server.tool(name, inputSchema.shape, async (args, extra) => { const input = args; const endpoint = typeof endpointTemplate === 'function' ? endpointTemplate(input) : endpointTemplate; const params = apiMethod === 'get' ? input : undefined; const data = (apiMethod === 'post' || apiMethod === 'put') ? (inputTransformer ? inputTransformer(input) : input) : undefined; try { const result = await callJobNimbusApi(apiMethod, endpoint, params, data); return { content: [{ type: "text", text: JSON.stringify(result, null, 2), }], }; } catch (error) { return { content: [{ type: "text", text: `Error calling ${name}: ${error.message || JSON.stringify(error)}`, }], isError: true, }; } }); } // --- Contacts API Tools --- // 1. List Contacts const ListContactsInputSchema = z.object({ page: z.number().int().optional(), size: z.number().int().optional(), search: z.string().optional(), sort_by: z.string().optional(), sort_dir: z.enum(["ASC", "DESC"]).optional(), status: z.string().optional(), // Filter by status name contact_type: z.string().optional(), // Filter by contact type name }).strict(); createTool("jobnimbus_list_contacts", ListContactsInputSchema, 'get', '/contacts'); // 2. Get Contact const GetContactInputSchema = z.object({ id: z.string(), // JNID of the contact }).strict(); createTool("jobnimbus_get_contact", GetContactInputSchema, 'get', (input) => `/contacts/${input.id}`); // 3. Create Contact // Define required and optional fields based on typical usage; JN API might have specific minimums const CreateContactInputSchema = z.object({ first_name: z.string().optional(), last_name: z.string().optional(), company_name: z.string().optional(), email: z.string().email().optional(), phone: z.string().optional(), address_line_1: z.string().optional(), city: z.string().optional(), state_text: z.string().optional(), zip: z.string().optional(), // Add other relevant fields as needed, mark as optional status: z.string().optional(), // e.g., "Lead" contact_type: z.string().optional(), // e.g., "Customer" // Ensure at least one identifying field is present if required by API (e.g., name/company) }).passthrough(); // Use passthrough to allow any other valid JN fields createTool("jobnimbus_create_contact", CreateContactInputSchema, 'post', '/contacts'); // 4. Update Contact const UpdateContactInputSchema = z.object({ id: z.string(), // JNID of the contact to update // Include all fields from Create, but all are optional for update first_name: z.string().optional(), last_name: z.string().optional(), company_name: z.string().optional(), email: z.string().email().optional(), phone: z.string().optional(), address_line_1: z.string().optional(), city: z.string().optional(), state_text: z.string().optional(), zip: z.string().optional(), status: z.string().optional(), contact_type: z.string().optional(), }).passthrough(); // Allow any other valid JN fields // Need a custom transformer for Update to remove the 'id' from the body createTool("jobnimbus_update_contact", UpdateContactInputSchema, 'put', (input) => `/contacts/${input.id}`, (input) => { const { id, ...updateData } = input; // Exclude 'id' from the data payload return updateData; }); // --- Jobs API Tools --- // 1. List Jobs const ListJobsInputSchema = z.object({ page: z.number().int().optional(), size: z.number().int().optional(), search: z.string().optional(), sort_by: z.string().optional(), sort_dir: z.enum(["ASC", "DESC"]).optional(), primary_id: z.string().optional(), // Filter by primary contact/lead JNID status: z.string().optional(), // Filter by status name job_type: z.string().optional(), // Filter by job type }).strict(); createTool("jobnimbus_list_jobs", ListJobsInputSchema, 'get', '/jobs'); // 2. Get Job const GetJobInputSchema = z.object({ id: z.string(), // JNID of the job }).strict(); createTool("jobnimbus_get_job", GetJobInputSchema, 'get', (input) => `/jobs/${input.id}`); // 3. Create Job // Updated to match the actual API schema based on real-world examples const CreateJobInputSchema = z.object({ name: z.string().optional(), number: z.string().optional(), description: z.string().optional(), // Updated field names to match API record_type_name: z.string().optional(), // Instead of job_type status_name: z.string().optional(), // Instead of status // Support for nested primary contact primary: z.object({ id: z.string() }).optional(), // Support for geo coordinates geo: z.object({ lat: z.number(), lon: z.number() }).optional(), // Keep address fields address_line_1: z.string().optional(), city: z.string().optional(), state_text: z.string().optional(), zip: z.string().optional(), // Keep backward compatibility for direct primary_id primary_id: z.string().optional(), // Will be transformed to primary.id if used }).passthrough(); // Allow any other valid JN job fields // Update createTool with a transformer to handle primary_id to primary.id conversion createTool("jobnimbus_create_job", CreateJobInputSchema, 'post', '/jobs', (input) => { // Create a copy of the input to modify const transformedInput = { ...input }; // If primary_id is provided but primary is not, convert it to the nested format if (transformedInput.primary_id && !transformedInput.primary) { transformedInput.primary = { id: transformedInput.primary_id }; // Remove the flat primary_id field delete transformedInput.primary_id; } return transformedInput; }); // 4. Update Job const UpdateJobInputSchema = z.object({ id: z.string(), // JNID of the job to update // All fields are optional for update name: z.string().optional(), number: z.string().optional(), description: z.string().optional(), // Updated field names to match API record_type_name: z.string().optional(), // Instead of job_type status_name: z.string().optional(), // Instead of status // Support for nested primary contact primary: z.object({ id: z.string() }).optional(), // Support for geo coordinates geo: z.object({ lat: z.number(), lon: z.number() }).optional(), address_line_1: z.string().optional(), city: z.string().optional(), state_text: z.string().optional(), zip: z.string().optional(), // Keep backward compatibility for direct primary_id primary_id: z.string().optional(), // Will be transformed to primary.id if used }).passthrough(); // Allow any other valid JN job fields createTool("jobnimbus_update_job", UpdateJobInputSchema, 'put', (input) => `/jobs/${input.id}`, (input) => { // Create a copy to avoid modifying the original input const { id, ...updateData } = { ...input }; // If primary_id is provided but primary is not, convert it to the nested format if (updateData.primary_id && !updateData.primary) { updateData.primary = { id: updateData.primary_id }; // Remove the flat primary_id field delete updateData.primary_id; } return updateData; }); // --- Workflows API Tools --- // 1. Get All Workflows (from Settings) // Input is empty, no params needed for this specific endpoint const GetAllWorkflowsInputSchema = z.object({}).strict(); server.tool("jobnimbus_get_all_workflows", GetAllWorkflowsInputSchema.shape, async () => { const endpoint = '/account/settings'; try { // Directly call the API helper as this endpoint has a unique structure const settingsResult = await callJobNimbusApi(// Use 'any' for now, refine if settings structure is known 'get', endpoint); // Extract only the workflows array as per backend.mdc const workflows = settingsResult?.workflows || []; return { content: [{ type: "text", text: JSON.stringify(workflows, null, 2), }], }; } catch (error) { return { content: [{ type: "text", text: `Error calling jobnimbus_get_all_workflows: ${error.message || JSON.stringify(error)}`, }], isError: true, }; } }); // 2. Create Workflow // IMPORTANT: Body requires fields directly, not nested under a 'params' key (handled by createTool) const CreateWorkflowInputSchema = z.object({ name: z.string(), // Workflow name, required parameter object_type: z.string(), // Workflow object type, required parameter (valid values: contact, job, workorder) record_type_id: z.number().int().optional(), // Integer ID (e.g., 40=Contact, 50=Job) is_sub_contractor: z.boolean().optional().default(false), // Optional field can_access_by_all: z.boolean().optional().default(false), // Optional field is_vendor: z.boolean().optional().default(false), // Optional field is_active: z.boolean().optional().default(true), // Optional field is_supplier: z.boolean().optional().default(false), // Optional field description: z.string().optional(), // Keep the description field }).passthrough(); // Allow additional fields the API might accept createTool("jobnimbus_create_workflow", CreateWorkflowInputSchema, 'post', '/account/workflow'); // 3. Create Workflow Status const CreateWorkflowStatusInputSchema = z.object({ workflow_id: z.number().int(), // This will be part of the URL path name: z.string(), // Status name, required parameter is_lead: z.boolean().optional().default(false), // Optional field is_closed: z.boolean().optional().default(false), // Optional field is_archived: z.boolean().optional().default(false), // Optional field send_to_quickbooks: z.boolean().optional().default(false), // Optional field force_mobile_sync: z.boolean().optional().default(false), // Optional field is_active: z.boolean().optional().default(true), // Optional field color: z.string().optional(), // Keep the color field from previous implementation sort_order: z.number().int().optional(), // Keep the sort_order field from previous implementation }).passthrough(); // Changed from strict() to passthrough() to allow additional fields // Need custom handling because workflow_id is in the path, not the body server.tool("jobnimbus_create_workflow_status", CreateWorkflowStatusInputSchema.shape, async (input) => { const { workflow_id, ...statusData } = input; const endpoint = `/account/workflow/${workflow_id}/status`; try { const result = await callJobNimbusApi('post', endpoint, undefined, statusData); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error calling jobnimbus_create_workflow_status: ${error.message || JSON.stringify(error)}`, }], isError: true, }; } }); // --- Tasks API Tools --- // 1. List Tasks const ListTasksInputSchema = z.object({ page: z.number().int().optional(), size: z.number().int().optional(), search: z.string().optional(), sort_by: z.string().optional(), sort_dir: z.enum(["ASC", "DESC"]).optional(), status: z.string().optional(), // Filter by status name assignee: z.string().optional(), // Filter by assignee JNID related_contact: z.string().optional(), // Filter by related contact JNID related_job: z.string().optional(), // Filter by related job JNID }).strict(); createTool("jobnimbus_list_tasks", ListTasksInputSchema, 'get', '/tasks'); // 2. Get Task const GetTaskInputSchema = z.object({ id: z.string(), // JNID of the task }).strict(); createTool("jobnimbus_get_task", GetTaskInputSchema, 'get', (input) => `/tasks/${input.id}`); // 3. Create Task // Updated to match the actual API schema with fields from the sample curl request const CreateTaskInputSchema = z.object({ title: z.string(), // Main field for task title from curl example description: z.string().optional(), status: z.string().optional(), priority: z.string().optional(), // e.g., "High", "Medium", "Low" due_date: z.string().datetime({ offset: true }).optional(), // ISO 8601 format date_start: z.number().int().optional(), // Unix timestamp from curl example date_end: z.number().int().optional(), // Unix timestamp from curl example record_type: z.number().int().optional(), // Numeric type ID from curl example record_type_name: z.string().optional(), // Type name from curl example (e.g., "Appointment") // Related entities - support both formats assignees: z.array(z.string()).optional(), // Array of assignee JNIDs related_contacts: z.array(z.string()).optional(), // Array of related contact JNIDs related_jobs: z.array(z.string()).optional(), // Array of related job JNIDs related: z.array(z.object({ id: z.string() })).optional(), // Alternative format from curl example }).passthrough(); // Simple direct tool without transformation since field names match API createTool("jobnimbus_create_task", CreateTaskInputSchema, 'post', '/tasks'); // 4. Update Task const UpdateTaskInputSchema = z.object({ id: z.string(), // JNID of the task to update // All fields optional for update title: z.string().optional(), // Main field for task title description: z.string().optional(), status: z.string().optional(), priority: z.string().optional(), due_date: z.string().datetime({ offset: true }).optional(), date_start: z.number().int().optional(), // Unix timestamp date_end: z.number().int().optional(), // Unix timestamp record_type: z.number().int().optional(), record_type_name: z.string().optional(), assignees: z.array(z.string()).optional(), related_contacts: z.array(z.string()).optional(), related_jobs: z.array(z.string()).optional(), related: z.array(z.object({ id: z.string() })).optional(), // Format from curl example }).passthrough(); createTool("jobnimbus_update_task", UpdateTaskInputSchema, 'put', (input) => `/tasks/${input.id}`, (input) => { const { id, ...updateData } = input; return updateData; }); // --- Products API Tools (v2 endpoint) --- const productsApiBase = "/api1/v2"; // Note the different base for products // Helper specific for product API calls function createProductTool(name, inputSchema, apiMethod, endpointTemplate, inputTransformer) { server.tool(name, inputSchema.shape, async (args, extra) => { const input = args; const endpointPath = typeof endpointTemplate === 'function' ? endpointTemplate(input) : endpointTemplate; const fullEndpoint = `${productsApiBase}${endpointPath}`; // Prepend v2 base const params = apiMethod === 'get' ? input : undefined; const data = (apiMethod === 'post' || apiMethod === 'put') ? (inputTransformer ? inputTransformer(input) : input) : undefined; try { // Use the main callJobNimbusApi, but provide the full endpoint const result = await callJobNimbusApi(apiMethod, fullEndpoint, params, data); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], }; } catch (error) { return { content: [{ type: "text", text: `Error calling ${name}: ${error.message || JSON.stringify(error)}`, }], isError: true, }; } }); } // 1. List Products const ListProductsInputSchema = z.object({ page: z.number().int().optional(), size: z.number().int().optional(), search: z.string().optional(), sort_by: z.string().optional(), sort_dir: z.enum(["ASC", "DESC"]).optional(), category: z.string().optional(), status: z.string().optional(), }).strict(); createProductTool("jobnimbus_list_products", ListProductsInputSchema, 'get', '/products'); // 2. Get Product const GetProductInputSchema = z.object({ id: z.string(), // JNID of the product }).strict(); createProductTool("jobnimbus_get_product", GetProductInputSchema, 'get', (input) => `/products/${input.id}`); // 3. Create Product // Updated to match the actual API schema with UOMs and pricing structure const CreateProductInputSchema = z.object({ name: z.string(), description: z.string().optional(), sku: z.string().optional(), // New fields from the curl example location_id: z.number().int().optional(), is_active: z.boolean().optional(), tax_exempt: z.boolean().optional(), item_type: z.string().optional(), // e.g., "labor+material", "material", "labor" suppliers: z.array(z.any()).optional(), // Array of suppliers // Multiple Units of Measure with nested pricing uoms: z.array(z.object({ uom: z.string(), // e.g., "SQ YD", "Labor" material: z.object({ cost: z.number(), price: z.number() }) })).optional(), // Keep these for backward compatibility price: z.number().optional(), cost: z.number().optional(), category: z.string().optional(), status: z.string().optional(), // e.g., "Active" unit_of_measure: z.string().optional(), tax_rate: z.number().optional(), }).passthrough(); createProductTool("jobnimbus_create_product", CreateProductInputSchema, 'post', '/products'); // 4. Update Product const UpdateProductInputSchema = z.object({ id: z.string(), // JNID of the product to update // All fields optional for update name: z.string().optional(), description: z.string().optional(), sku: z.string().optional(), jnid: z.string().optional(), // Some APIs include jnid in the body // New fields from the curl example location_id: z.number().int().optional(), is_active: z.boolean().optional(), tax_exempt: z.boolean().optional(), item_type: z.string().optional(), // e.g., "labor+material", "material", "labor" suppliers: z.array(z.any()).optional(), // Array of suppliers // Multiple Units of Measure with nested pricing uoms: z.array(z.object({ uom: z.string(), // e.g., "SQ YD", "Labor" material: z.object({ cost: z.number(), price: z.number() }) })).optional(), // Keep backward compatibility fields price: z.number().optional(), cost: z.number().optional(), category: z.string().optional(), status: z.string().optional(), unit_of_measure: z.string().optional(), tax_rate: z.number().optional(), inventory_count: z.number().int().optional(), }).passthrough(); createProductTool("jobnimbus_update_product", UpdateProductInputSchema, 'put', (input) => `/products/${input.id}`, (input) => { const { id, ...updateData } = { ...input }; return updateData; }); // --- Invoices API Tools --- // 1. List Invoices const ListInvoicesInputSchema = z.object({ page: z.number().int().optional(), size: z.number().int().optional(), search: z.string().optional(), sort_by: z.string().optional(), sort_dir: z.enum(["ASC", "DESC"]).optional(), primary_id: z.string().optional(), // Filter by primary contact/customer JNID job_id: z.string().optional(), // Filter by related job JNID status: z.string().optional(), // Filter by invoice status ("Draft", "Sent", "Paid", etc.) date_from: z.string().optional(), // Filter by date range start date_to: z.string().optional(), // Filter by date range end }).strict(); createTool("jobnimbus_list_invoices", ListInvoicesInputSchema, 'get', '/invoices'); // 2. Get Invoice const GetInvoiceInputSchema = z.object({ id: z.string(), // JNID of the invoice }).strict(); createTool("jobnimbus_get_invoice", GetInvoiceInputSchema, 'get', (input) => `/invoices/${input.id}`); // 3. Create Invoice const CreateInvoiceInputSchema = z.object({ // Basic invoice details number: z.string().optional(), // Invoice number title: z.string().optional(), // Invoice title description: z.string().optional(), // Invoice description status: z.string().optional(), // Invoice status (e.g., "Draft", "Sent") date: z.string().optional(), // Invoice date (ISO format) due_date: z.string().optional(), // Due date (ISO format) terms: z.string().optional(), // Payment terms // Amounts and totals subtotal: z.number().optional(), // Subtotal amount tax_amount: z.number().optional(), // Tax amount tax_rate: z.number().optional(), // Tax rate percentage discount_amount: z.number().optional(), // Discount amount discount_rate: z.number().optional(), // Discount rate percentage total: z.number().optional(), // Total amount // Related entities primary_id: z.string().optional(), // Customer JNID primary: z.object({ id: z.string() }).optional(), // Alternative format for customer job_id: z.string().optional(), // Related job JNID job: z.object({ id: z.string() }).optional(), // Alternative format for job // Line items line_items: z.array(z.object({ product_id: z.string().optional(), // JNID of product name: z.string(), // Line item name description: z.string().optional(), // Line item description quantity: z.number(), // Quantity unit_price: z.number(), // Unit price amount: z.number().optional(), // Total line amount tax_rate: z.number().optional(), // Line-specific tax rate is_taxable: z.boolean().optional(), // Whether line is taxable unit_of_measure: z.string().optional(), // Unit of measure })).optional(), }).passthrough(); // Transform input for create invoice createTool("jobnimbus_create_invoice", CreateInvoiceInputSchema, 'post', '/invoices', (input) => { // Create a copy to avoid modifying the original input const transformedInput = { ...input }; // Transform primary_id to nested format if needed if (transformedInput.primary_id && !transformedInput.primary) { transformedInput.primary = { id: transformedInput.primary_id }; delete transformedInput.primary_id; } // Transform job_id to nested format if needed if (transformedInput.job_id && !transformedInput.job) { transformedInput.job = { id: transformedInput.job_id }; delete transformedInput.job_id; } return transformedInput; }); // 4. Update Invoice const UpdateInvoiceInputSchema = z.object({ id: z.string(), // JNID of the invoice to update // All fields are optional for update number: z.string().optional(), title: z.string().optional(), description: z.string().optional(), status: z.string().optional(), date: z.string().optional(), due_date: z.string().optional(), terms: z.string().optional(), subtotal: z.number().optional(), tax_amount: z.number().optional(), tax_rate: z.number().optional(), discount_amount: z.number().optional(), discount_rate: z.number().optional(), total: z.number().optional(), primary_id: z.string().optional(), primary: z.object({ id: z.string() }).optional(), job_id: z.string().optional(), job: z.object({ id: z.string() }).optional(), line_items: z.array(z.object({ id: z.string().optional(), // Existing line item ID for updates product_id: z.string().optional(), name: z.string().optional(), description: z.string().optional(), quantity: z.number().optional(), unit_price: z.number().optional(), amount: z.number().optional(), tax_rate: z.number().optional(), is_taxable: z.boolean().optional(), unit_of_measure: z.string().optional(), })).optional(), }).passthrough(); createTool("jobnimbus_update_invoice", UpdateInvoiceInputSchema, 'put', (input) => `/invoices/${input.id}`, (input) => { // Create a copy to avoid modifying the original input const { id, ...updateData } = { ...input }; // Transform primary_id to nested format if needed if (updateData.primary_id && !updateData.primary) { updateData.primary = { id: updateData.primary_id }; delete updateData.primary_id; } // Transform job_id to nested format if needed if (updateData.job_id && !updateData.job) { updateData.job = { id: updateData.job_id }; delete updateData.job_id; } return updateData; }); // 5. Send Invoice const SendInvoiceInputSchema = z.object({ id: z.string(), // JNID of the invoice to send email_to: z.array(z.string().email()).optional(), // Array of email addresses to send to cc: z.array(z.string().email()).optional(), // Array of CC email addresses bcc: z.array(z.string().email()).optional(), // Array of BCC email addresses subject: z.string().optional(), // Email subject message: z.string().optional(), // Email message body attachments: z.array(z.string()).optional(), // Array of attachment IDs }).strict(); createTool("jobnimbus_send_invoice", SendInvoiceInputSchema, 'post', (input) => `/invoices/${input.id}/send`, (input) => { const { id, ...sendData } = input; return sendData; }); // 6. Record Payment const RecordInvoicePaymentInputSchema = z.object({ id: z.string(), // JNID of the invoice amount: z.number(), // Payment amount date: z.string(), // Payment date (ISO format) method: z.string().optional(), // Payment method (e.g., "Credit Card", "Check", "Cash") reference: z.string().optional(), // Payment reference (e.g., check number) notes: z.string().optional(), // Payment notes }).strict(); createTool("jobnimbus_record_invoice_payment", RecordInvoicePaymentInputSchema, 'post', (input) => `/invoices/${input.id}/payments`, (input) => { const { id, ...paymentData } = input; return paymentData; }); // --- Prompts --- // Job-Contact Relationship prompt server.prompt("job_contact_relationship", { contactInfo: z.string().optional() }, ({ contactInfo }) => ({ messages: [{ role: "user", content: { type: "text", text: ` When creating a job in JobNimbus, remember these important rules: 1. Every job MUST have a primary contact (customer) associated with it 2. The "primary.id" field in the job creation request must contain a valid contact JNID 3. Before creating a job, you must verify if the contact exists: - Use jobnimbus_list_contacts with appropriate search parameters - If the contact doesn't exist, first create it with jobnimbus_create_contact - Use the returned contact JNID in the job creation request 4. Job creation sequence: a. Check contact → b. Create contact if needed → c. Create job with contact ID ${contactInfo ? `For the current request, check if this contact exists: ${contactInfo}` : ''} ` } }] })); // --- Connect and Start Server --- async function startServer() { try { console.error("Attempting to connect transport..."); const transport = new StdioServerTransport(); await server.connect(transport); console.error("JobNimbus MCP Server connected via stdio and listening..."); } catch (error) { console.error("Failed to connect MCP server:", error); process.exit(1); } } startServer(); //# sourceMappingURL=server.js.map