UNPKG

quartzy-mcp-server

Version:

Model Context Protocol server for Quartzy Public API - laboratory inventory and order management

673 lines 30.9 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; class QuartzyMCPServer { server; config; constructor() { this.server = new Server({ name: "quartzy-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, }); this.config = { accessToken: process.env.QUARTZY_ACCESS_TOKEN || "", baseUrl: process.env.QUARTZY_BASE_URL || "https://api.quartzy.com", }; this.setupToolHandlers(); this.setupErrorHandling(); } setupErrorHandling() { this.server.onerror = (error) => { console.error("[MCP Error]", error); }; process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ // Health Check { name: "quartzy_health_check", description: "Check the health status of the Quartzy API service", inputSchema: { type: "object", properties: {}, }, }, // User { name: "quartzy_get_current_user", description: "Get information about the current authenticated user", inputSchema: { type: "object", properties: {}, }, }, // Labs { name: "quartzy_list_labs", description: "Get a list of labs, optionally filtered by organization", inputSchema: { type: "object", properties: { organization_id: { type: "string", description: "The Organization ID to filter on (UUID format)", }, page: { type: "number", description: "The page of results to retrieve", }, }, }, }, { name: "quartzy_get_lab", description: "Get details of a specific lab by ID", inputSchema: { type: "object", properties: { id: { type: "string", description: "The UUID of the lab to retrieve", }, }, required: ["id"], }, }, // Inventory Items { name: "quartzy_list_inventory_items", description: "List and filter inventory items", inputSchema: { type: "object", properties: { page: { type: "number", description: "The page of results to retrieve", }, lab_id: { type: "string", description: "The Lab ID to filter on", }, }, }, }, { name: "quartzy_get_inventory_item", description: "Get details of a specific inventory item", inputSchema: { type: "object", properties: { id: { type: "string", description: "The UUID of the inventory item to retrieve", }, }, required: ["id"], }, }, { name: "quartzy_update_inventory_item_quantity", description: "Update the quantity of an inventory item", inputSchema: { type: "object", properties: { id: { type: "string", description: "The ID of the inventory item to update", }, quantity: { type: "string", description: "The new quantity value", }, }, required: ["id", "quantity"], }, }, // Order Requests { name: "quartzy_list_order_requests", description: "List and filter order requests", inputSchema: { type: "object", properties: { page: { type: "number", description: "The page of results to retrieve", }, lab_id: { type: "string", description: "The Lab ID to filter on", }, }, }, }, { name: "quartzy_get_order_request", description: "Get details of a specific order request", inputSchema: { type: "object", properties: { id: { type: "string", description: "The UUID of the order request to retrieve", }, }, required: ["id"], }, }, { name: "quartzy_create_order_request", description: "Create a new order request", inputSchema: { type: "object", properties: { lab_id: { type: "string", description: "The UUID of the lab", }, type_id: { type: "string", description: "The UUID of the type", }, name: { type: "string", description: "Name of the item being ordered", }, vendor_product_id: { type: "string", description: "The UUID of the vendor product (optional)", }, vendor_name: { type: "string", description: "Name of the vendor", }, catalog_number: { type: "string", description: "Vendor catalog number", }, price: { type: "object", properties: { amount: { type: "string", description: "Price amount as string integer", }, currency: { type: "string", description: "Currency code (e.g., USD)", }, }, required: ["amount", "currency"], }, quantity: { type: "number", description: "Quantity to order", }, required_before: { type: "string", description: "Required date in YYYY-MM-DD format", }, notes: { type: "string", description: "Additional notes", }, }, required: ["lab_id", "type_id", "name", "vendor_name", "catalog_number", "price", "quantity"], }, }, { name: "quartzy_update_order_request", description: "Update the status of an order request", inputSchema: { type: "object", properties: { id: { type: "string", description: "The UUID of the order request to update", }, status: { type: "string", enum: ["CREATED", "CANCELLED", "APPROVED", "ORDERED", "BACKORDERED", "RECEIVED"], description: "New status for the order request", }, }, required: ["id", "status"], }, }, // Types { name: "quartzy_list_types", description: "List and filter item types", inputSchema: { type: "object", properties: { lab_id: { type: "string", description: "The Lab UUID to filter on", }, name: { type: "string", description: "The Type Name to filter on", }, page: { type: "number", description: "The page of results to retrieve", }, }, }, }, // Webhooks { name: "quartzy_list_webhooks", description: "List and filter webhooks", inputSchema: { type: "object", properties: { organization_id: { type: "string", description: "The Organization UUID to filter on", }, page: { type: "number", description: "The page of results to retrieve", }, }, }, }, { name: "quartzy_get_webhook", description: "Get details of a specific webhook", inputSchema: { type: "object", properties: { id: { type: "string", description: "The UUID of the webhook to retrieve", }, }, required: ["id"], }, }, { name: "quartzy_create_webhook", description: "Create a new webhook for lab or organization events", inputSchema: { type: "object", properties: { lab_id: { type: "string", description: "The Lab UUID (use either lab_id or organization_id)", }, organization_id: { type: "string", description: "The Organization UUID (use either lab_id or organization_id)", }, name: { type: "string", description: "Name for the webhook", }, url: { type: "string", description: "URL endpoint for the webhook", }, event_types: { type: "array", items: { type: "string" }, description: "Array of event types to subscribe to", }, is_enabled: { type: "boolean", description: "Whether the webhook is enabled", }, is_verified: { type: "boolean", description: "Whether the webhook URL is verified", }, is_signed: { type: "boolean", description: "Whether webhook payloads should be signed", }, }, required: ["url"], }, }, { name: "quartzy_update_webhook", description: "Update webhook settings (currently only supports enabling/disabling)", inputSchema: { type: "object", properties: { id: { type: "string", description: "The UUID of the webhook to update", }, is_enabled: { type: "boolean", description: "Whether the webhook should be enabled", }, }, required: ["id", "is_enabled"], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { // Cast args to a more specific type for better type safety const typedArgs = args; switch (name) { case "quartzy_health_check": return await this.healthCheck(); case "quartzy_get_current_user": return await this.getCurrentUser(); case "quartzy_list_labs": return await this.listLabs(this.getStringArg(typedArgs, 'organization_id'), this.getNumberArg(typedArgs, 'page')); case "quartzy_get_lab": return await this.getLab(this.getRequiredStringArg(typedArgs, 'id')); case "quartzy_list_inventory_items": return await this.listInventoryItems(this.getStringArg(typedArgs, 'lab_id'), this.getNumberArg(typedArgs, 'page')); case "quartzy_get_inventory_item": return await this.getInventoryItem(this.getRequiredStringArg(typedArgs, 'id')); case "quartzy_update_inventory_item_quantity": return await this.updateInventoryItemQuantity(this.getRequiredStringArg(typedArgs, 'id'), this.getRequiredStringArg(typedArgs, 'quantity')); case "quartzy_list_order_requests": return await this.listOrderRequests(this.getStringArg(typedArgs, 'lab_id'), this.getNumberArg(typedArgs, 'page')); case "quartzy_get_order_request": return await this.getOrderRequest(this.getRequiredStringArg(typedArgs, 'id')); case "quartzy_create_order_request": return await this.createOrderRequest(typedArgs); case "quartzy_update_order_request": return await this.updateOrderRequest(this.getRequiredStringArg(typedArgs, 'id'), this.getRequiredStringArg(typedArgs, 'status')); case "quartzy_list_types": return await this.listTypes(this.getStringArg(typedArgs, 'lab_id'), this.getStringArg(typedArgs, 'name'), this.getNumberArg(typedArgs, 'page')); case "quartzy_list_webhooks": return await this.listWebhooks(this.getStringArg(typedArgs, 'organization_id'), this.getNumberArg(typedArgs, 'page')); case "quartzy_get_webhook": return await this.getWebhook(this.getRequiredStringArg(typedArgs, 'id')); case "quartzy_create_webhook": return await this.createWebhook(typedArgs); case "quartzy_update_webhook": return await this.updateWebhook(this.getRequiredStringArg(typedArgs, 'id'), this.getRequiredBooleanArg(typedArgs, 'is_enabled')); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); } // Type-safe argument extraction helpers getStringArg(args, key) { const value = args[key]; return typeof value === 'string' ? value : undefined; } getRequiredStringArg(args, key) { const value = this.getStringArg(args, key); if (!value) { throw new Error(`Required parameter '${key}' is missing or not a string`); } return value; } getNumberArg(args, key) { const value = args[key]; return typeof value === 'number' ? value : undefined; } getRequiredNumberArg(args, key) { const value = this.getNumberArg(args, key); if (value === undefined) { throw new Error(`Required parameter '${key}' is missing or not a number`); } return value; } getBooleanArg(args, key) { const value = args[key]; return typeof value === 'boolean' ? value : undefined; } getRequiredBooleanArg(args, key) { const value = this.getBooleanArg(args, key); if (value === undefined) { throw new Error(`Required parameter '${key}' is missing or not a boolean`); } return value; } async makeRequest(endpoint, method = "GET", body) { if (!this.config.accessToken) { throw new Error("QUARTZY_ACCESS_TOKEN environment variable is required"); } const url = `${this.config.baseUrl}${endpoint}`; const headers = { "Access-Token": this.config.accessToken, "Content-Type": "application/json", }; const config = { method, headers, }; if (body && (method === "POST" || method === "PUT")) { config.body = JSON.stringify(body); } const response = await fetch(url, config); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } // Handle empty responses (e.g., 204 No Content) if (response.status === 204 || response.headers.get("content-length") === "0") { return null; } return await response.json(); } formatResponse(data, description) { return { content: [ { type: "text", text: `${description}\n\n${JSON.stringify(data, null, 2)}`, }, ], }; } // API Methods async healthCheck() { const data = await this.makeRequest("/healthz"); return this.formatResponse(data, "Quartzy API Health Status:"); } async getCurrentUser() { const data = await this.makeRequest("/user"); return this.formatResponse(data, "Current User Information:"); } async listLabs(organizationId, page) { let endpoint = "/labs"; const params = new URLSearchParams(); if (organizationId) params.append("organization_id", organizationId); if (page) params.append("page", page.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const data = await this.makeRequest(endpoint); return this.formatResponse(data, "Labs:"); } async getLab(id) { if (!id) throw new Error("Lab ID is required"); const data = await this.makeRequest(`/labs/${id}`); return this.formatResponse(data, `Lab Details (${id}):`); } async listInventoryItems(labId, page) { let endpoint = "/inventory-items"; const params = new URLSearchParams(); if (labId) params.append("lab_id", labId); if (page) params.append("page", page.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const data = await this.makeRequest(endpoint); return this.formatResponse(data, "Inventory Items:"); } async getInventoryItem(id) { if (!id) throw new Error("Inventory Item ID is required"); const data = await this.makeRequest(`/inventory-items/${id}`); return this.formatResponse(data, `Inventory Item Details (${id}):`); } async updateInventoryItemQuantity(id, quantity) { if (!id || !quantity) throw new Error("Both ID and quantity are required"); const data = await this.makeRequest(`/inventory-items/${id}`, "PUT", { quantity }); return this.formatResponse(data, `Updated Inventory Item (${id}):`); } async listOrderRequests(labId, page) { let endpoint = "/order-requests"; const params = new URLSearchParams(); if (labId) params.append("lab_id", labId); if (page) params.append("page", page.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const data = await this.makeRequest(endpoint); return this.formatResponse(data, "Order Requests:"); } async getOrderRequest(id) { if (!id) throw new Error("Order Request ID is required"); const data = await this.makeRequest(`/order-requests/${id}`); return this.formatResponse(data, `Order Request Details (${id}):`); } async createOrderRequest(args) { const requiredFields = ["lab_id", "type_id", "name", "vendor_name", "catalog_number", "price", "quantity"]; // Validate required fields for (const field of requiredFields) { if (!args[field]) { throw new Error(`${field} is required`); } } // Build the request object with proper typing const requestData = { lab_id: this.getRequiredStringArg(args, 'lab_id'), type_id: this.getRequiredStringArg(args, 'type_id'), name: this.getRequiredStringArg(args, 'name'), vendor_name: this.getRequiredStringArg(args, 'vendor_name'), catalog_number: this.getRequiredStringArg(args, 'catalog_number'), price: args.price, // This should be an object, we'll validate it in the API call quantity: args.quantity, // This should be a number vendor_product_id: this.getStringArg(args, 'vendor_product_id'), required_before: this.getStringArg(args, 'required_before'), notes: this.getStringArg(args, 'notes'), }; // Remove undefined values const cleanedData = Object.fromEntries(Object.entries(requestData).filter(([_, value]) => value !== undefined)); const data = await this.makeRequest("/order-requests", "POST", cleanedData); return this.formatResponse(data, "Created Order Request:"); } async updateOrderRequest(id, status) { if (!id || !status) throw new Error("Both ID and status are required"); const validStatuses = ["CREATED", "CANCELLED", "APPROVED", "ORDERED", "BACKORDERED", "RECEIVED"]; if (!validStatuses.includes(status)) { throw new Error(`Status must be one of: ${validStatuses.join(", ")}`); } const data = await this.makeRequest(`/order-requests/${id}`, "PUT", { status }); return this.formatResponse(data, `Updated Order Request (${id}):`); } async listTypes(labId, name, page) { let endpoint = "/types"; const params = new URLSearchParams(); if (labId) params.append("lab_id", labId); if (name) params.append("name", name); if (page) params.append("page", page.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const data = await this.makeRequest(endpoint); return this.formatResponse(data, "Types:"); } async listWebhooks(organizationId, page) { let endpoint = "/webhooks"; const params = new URLSearchParams(); if (organizationId) params.append("organization_id", organizationId); if (page) params.append("page", page.toString()); if (params.toString()) { endpoint += `?${params.toString()}`; } const data = await this.makeRequest(endpoint); return this.formatResponse(data, "Webhooks:"); } async getWebhook(id) { if (!id) throw new Error("Webhook ID is required"); const data = await this.makeRequest(`/webhooks/${id}`); return this.formatResponse(data, `Webhook Details (${id}):`); } async createWebhook(args) { const url = this.getRequiredStringArg(args, 'url'); // Must have either lab_id or organization_id const labId = this.getStringArg(args, 'lab_id'); const organizationId = this.getStringArg(args, 'organization_id'); if (!labId && !organizationId) { throw new Error("Either lab_id or organization_id is required"); } // Build the request object const requestData = { url, lab_id: labId, organization_id: organizationId, name: this.getStringArg(args, 'name'), event_types: args.event_types, // Should be an array is_enabled: this.getBooleanArg(args, 'is_enabled'), is_verified: this.getBooleanArg(args, 'is_verified'), is_signed: this.getBooleanArg(args, 'is_signed'), }; // Remove undefined values const cleanedData = Object.fromEntries(Object.entries(requestData).filter(([_, value]) => value !== undefined)); const data = await this.makeRequest("/webhooks", "POST", cleanedData); return this.formatResponse(data, "Created Webhook:"); } async updateWebhook(id, isEnabled) { if (!id || typeof isEnabled !== "boolean") { throw new Error("Both ID and is_enabled (boolean) are required"); } const data = await this.makeRequest(`/webhooks/${id}`, "PUT", { is_enabled: isEnabled }); return this.formatResponse(data, `Updated Webhook (${id}):`); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Quartzy MCP Server running on stdio"); } } const server = new QuartzyMCPServer(); server.run().catch(console.error); //# sourceMappingURL=index.js.map