index.js•9.19 kB
#!/usr/bin/env node
// @ts-check
/**
* MCP Server for Inflow Inventory Integration
* Provides tools to manage products/ingredients and inventory
*/
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 { InflowClient } from './src/inflow-client.js';
import { productHandlers } from './src/handlers/product-handlers.js';
import { inventoryHandlers } from './src/handlers/inventory-handlers.js';
// Load environment variables
dotenv.config();
/** @typedef {import('./types').InflowConfig} InflowConfig */
/** @typedef {import('./types').MCPResponse} MCPResponse */
/** @typedef {import('./types').MCPToolResult} MCPToolResult */
// Validate environment variables
if (!process.env.INFLOW_API_KEY) {
console.error('Error: INFLOW_API_KEY is required in environment variables');
process.exit(1);
}
if (!process.env.INFLOW_COMPANY_ID) {
console.error('Error: INFLOW_COMPANY_ID is required in environment variables');
process.exit(1);
}
/** @type {InflowConfig} */
const config = {
apiKey: process.env.INFLOW_API_KEY,
companyId: process.env.INFLOW_COMPANY_ID,
apiUrl: process.env.INFLOW_API_URL || 'https://cloudapi.inflowinventory.com',
apiVersion: process.env.INFLOW_API_VERSION || '2025-06-24'
};
// Initialize Inflow client
const inflowClient = new InflowClient(config);
// Initialize MCP Server
const server = new McpServer({
name: 'mcp-inflow-ingredients',
version: '1.0.0'
});
/**
* Register all available tools
*/
// List Ingredients (Products)
server.registerTool(
'list_ingredients',
{
description: 'List all ingredients/products in Inflow inventory with optional filters',
inputSchema: {
name: z.string().optional().describe('Filter by product name'),
description: z.string().optional().describe('Filter by description'),
isActive: z.boolean().optional().describe('Filter by active status'),
barcode: z.string().optional().describe('Filter by barcode'),
smart: z.string().optional().describe('Full-text search across name, description, SKU, barcode'),
include: z.string().optional().describe('Related entities to include (e.g., "inventoryLines,defaultImage")'),
limit: z.number().optional().describe('Maximum number of results (default: 50)')
}
},
async (args) => {
const result = await productHandlers.listProducts(inflowClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
);
// Get Ingredient Details
server.registerTool(
'get_ingredient',
{
description: 'Get detailed information about a specific ingredient/product',
inputSchema: {
productId: z.string().describe('The product ID (UUID)'),
include: z.string().optional().describe('Related entities to include (e.g., "inventoryLines,defaultImage")')
}
},
async (args) => {
const result = await productHandlers.getProduct(inflowClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
);
// Get Inventory Summary
server.registerTool(
'get_inventory_summary',
{
description: 'Get inventory summary for a product including quantities on hand, available, reserved, etc.',
inputSchema: {
productId: z.string().describe('The product ID (UUID)')
}
},
async (args) => {
const result = await productHandlers.getProductSummary(inflowClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
);
// Search Ingredients
server.registerTool(
'search_ingredients',
{
description: 'Search for ingredients using full-text search across name, description, SKU, and barcode',
inputSchema: {
query: z.string().describe('Search query string'),
limit: z.number().optional().describe('Maximum number of results (default: 25)'),
include: z.string().optional().describe('Related entities to include')
}
},
async (args) => {
const result = await productHandlers.searchProducts(inflowClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
);
// Create Ingredient
server.registerTool(
'create_ingredient',
{
description: 'Create a new ingredient/product in Inflow inventory',
inputSchema: {
productId: z.string().describe('UUID for the new product (generate with crypto.randomUUID())'),
name: z.string().describe('Product name'),
sku: z.string().optional().describe('SKU code'),
description: z.string().optional().describe('Product description'),
isActive: z.boolean().optional().describe('Active status (default: true)'),
additionalFields: z.record(z.any()).optional().describe('Any additional product fields')
}
},
async (args) => {
const result = await productHandlers.createProduct(inflowClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
);
// Update Ingredient
server.registerTool(
'update_ingredient',
{
description: 'Update an existing ingredient/product in Inflow inventory',
inputSchema: {
productId: z.string().describe('The product ID to update'),
name: z.string().optional().describe('New product name'),
sku: z.string().optional().describe('New SKU code'),
description: z.string().optional().describe('New description'),
isActive: z.boolean().optional().describe('Active status'),
additionalFields: z.record(z.any()).optional().describe('Any additional fields to update')
}
},
async (args) => {
const result = await productHandlers.updateProduct(inflowClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
);
// Create Stock Adjustment
server.registerTool(
'create_stock_adjustment',
{
description: 'Create a stock adjustment to modify inventory quantities for one or more products',
inputSchema: {
stockAdjustmentId: z.string().describe('UUID for the adjustment (generate with crypto.randomUUID())'),
locationId: z.string().describe('Location ID where adjustment occurs'),
lines: z.array(z.object({
productId: z.string().describe('Product ID being adjusted'),
quantity: z.number().describe('Quantity to adjust (can be negative for reductions)')
})).describe('Array of products and quantities to adjust'),
adjustmentReasonId: z.string().optional().describe('Reason ID for the adjustment'),
notes: z.string().optional().describe('Notes about the adjustment'),
adjustmentDate: z.string().optional().describe('Date of adjustment (ISO format)')
}
},
async (args) => {
const result = await inventoryHandlers.createStockAdjustment(inflowClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
);
// List Stock Adjustments
server.registerTool(
'list_stock_adjustments',
{
description: 'List stock adjustments with optional filters',
inputSchema: {
adjustmentNumber: z.string().optional().describe('Filter by adjustment number'),
include: z.string().optional().describe('Related entities to include'),
limit: z.number().optional().describe('Maximum number of results (default: 50)')
}
},
async (args) => {
const result = await inventoryHandlers.listStockAdjustments(inflowClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
);
// Get Stock Adjustment
server.registerTool(
'get_stock_adjustment',
{
description: 'Get details of a specific stock adjustment',
inputSchema: {
stockAdjustmentId: z.string().describe('The stock adjustment ID'),
include: z.string().optional().describe('Related entities to include')
}
},
async (args) => {
const result = await inventoryHandlers.getStockAdjustment(inflowClient, args);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2)
}
]
};
}
);
/**
* Start the server
*/
async function main() {
const transport = new StdioServerTransport();
try {
await server.connect(transport);
console.error('Inflow Inventory MCP Server started successfully');
console.error(`API URL: ${config.apiUrl}`);
console.error(`Company ID: ${config.companyId}`);
console.error(`API Version: ${config.apiVersion}`);
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
// Handle errors
process.on('unhandledRejection', (error) => {
console.error('Unhandled rejection:', error);
process.exit(1);
});
// Start server
main();