diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..7bb7345 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,63 @@ +name: Validate onX Compliance + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + validate: + runs-on: ubuntu-latest + defaults: + run: + working-directory: . + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install server dependencies + run: npm ci + working-directory: ./server + + - name: Build server + run: npm run build + working-directory: ./server + + - name: Install validator dependencies + run: npm ci + working-directory: ./validator + + - name: Build validator + run: npm run build + working-directory: ./validator + + - name: Validate MCP Server + working-directory: ./validator + env: + ADAPTER_TYPE: built-in + ADAPTER_NAME: mock + run: | + node dist/cli/index.js stdio node -a "../server/dist/index.js" + + - name: Generate JSON Report + if: always() + working-directory: ./validator + env: + ADAPTER_TYPE: built-in + ADAPTER_NAME: mock + run: | + node dist/cli/index.js stdio node -a "../server/dist/index.js" -f json > compliance-report.json || true + cat compliance-report.json + + - name: Upload Compliance Report + if: always() + uses: actions/upload-artifact@v4 + with: + name: compliance-report + path: validator/compliance-report.json diff --git a/.gitignore b/.gitignore index 72a21d8..e067b29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,36 +1,37 @@ # Node.js dependencies -server/node_modules/ -server/npm-debug.log* -server/yarn-debug.log* -server/yarn-error.log* +node_modules/ + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* # Environment variables -server/.env -server/.env.local -server/.env.*.local +.env +.env.local +.env.*.local # Build output -server/dist/ -server/build/ -server/coverage/ -server/.next/ -server/.nuxt/ -server/.cache/ -server/.parcel-cache/ +dist/ +build/ +coverage/ +.next/ +.nuxt/ +.cache/ +.parcel-cache/ # OS files .DS_Store Thumbs.db # Logs -server/logs/ -server/*.log +logs/ +*.log # Temporary files tmp/ -server/tmp/ -server/temp/ -server/.tmp/ +temp/ +.tmp/ working-docs/ # IDE files diff --git a/schemas/index.js b/schemas/index.js new file mode 100644 index 0000000..f2077a2 --- /dev/null +++ b/schemas/index.js @@ -0,0 +1,55 @@ +/** + * onX Schema Package + * Canonical definitions for the Order Network eXchange specification + */ + +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); + +/** + * Tools required for onX spec compliance + */ +export const ONX_TOOLS = [ + // Order operations + 'create-sales-order', + 'update-order', + 'cancel-order', + 'fulfill-order', + 'create-return', + + // Query operations + 'get-orders', + 'get-customers', + 'get-products', + 'get-product-variants', + 'get-inventory', + 'get-fulfillments', + 'get-returns', +]; + +/** + * Load schemas for all tools + */ +function loadSchemas() { + const schemas = new Map(); + + for (const toolName of ONX_TOOLS) { + try { + const schema = require(`./tool-inputs/${toolName}.json`); + schemas.set(toolName, schema); + } catch { + console.warn(`Warning: Schema file missing for ${toolName}`); + } + } + + return schemas; +} + +// Export schemas as a Map (loaded once at module initialization) +export const toolInputSchemas = loadSchemas(); + +// Export getter for individual schema +export function getToolInputSchema(toolName) { + return toolInputSchemas.get(toolName) || null; +} diff --git a/schemas/package.json b/schemas/package.json new file mode 100644 index 0000000..b721d18 --- /dev/null +++ b/schemas/package.json @@ -0,0 +1,9 @@ +{ + "name": "@onx/schemas", + "version": "1.0.0", + "description": "Canonical onX data schemas", + "type": "module", + "exports": { + ".": "./index.js" + } +} diff --git a/schemas/tool-inputs/create-return.json b/schemas/tool-inputs/create-return.json new file mode 100644 index 0000000..d7f2b25 --- /dev/null +++ b/schemas/tool-inputs/create-return.json @@ -0,0 +1,466 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "create-return", + "description": "Input schema for creating a return", + "type": "object", + "properties": { + "return": { + "type": "object", + "properties": { + "externalId": { + "description": "ID of the entity in the client's system. Must be unique within the tenant.", + "type": "string" + }, + "returnNumber": { + "description": "Customer-facing return identifier used for tracking and reference (e.g., \"RET-12345\")", + "type": "string" + }, + "orderId": { + "description": "ID of the original order being returned", + "type": "string" + }, + "status": { + "description": "Return processing status in the return lifecycle", + "type": "string" + }, + "outcome": { + "description": "What the customer receives for their return", + "type": "string" + }, + "returnLineItems": { + "description": "Items being returned", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "description": "Unique identifier for this return line item", + "type": "string" + }, + "orderLineItemId": { + "description": "Reference to the original order line item", + "type": "string" + }, + "sku": { + "description": "Product Variant SKU", + "type": "string" + }, + "quantityReturned": { + "description": "Quantity being returned", + "type": "number", + "minimum": 1 + }, + "returnReason": { + "description": "Primary return reason code (e.g., \"defective\", \"wrong_item\", \"no_longer_needed\", \"size_issue\", \"quality_issue\")", + "type": "string" + }, + "inspection": { + "type": "object", + "properties": { + "conditionCategory": { + "description": "Item condition grade after inspection", + "type": "string" + }, + "dispositionOutcome": { + "description": "Disposition decision for the returned item", + "type": "string" + }, + "warehouseLocationId": { + "description": "Warehouse bin/shelf location identifier for restocking", + "type": "string" + }, + "note": { + "description": "Inspection notes about item condition and disposition", + "type": "string" + }, + "inspectedBy": { + "description": "Who inspected the item", + "type": "string" + }, + "inspectedAt": { + "description": "When item was inspected", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "images": { + "description": "Photos of returned item condition", + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + } + }, + "additionalProperties": false + }, + "unitPrice": { + "description": "Original unit price from order", + "type": "number" + }, + "refundAmount": { + "description": "Refund amount for this line item", + "type": "number", + "minimum": 0 + }, + "restockFee": { + "description": "Restocking fee charged for this line item", + "type": "number", + "minimum": 0 + }, + "name": { + "description": "Product name for display", + "type": "string" + } + }, + "required": [ + "orderLineItemId", + "sku", + "quantityReturned", + "returnReason" + ], + "additionalProperties": false + } + }, + "exchangeLineItems": { + "description": "Items being exchanged", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "description": "Unique exchange line item identifier", + "type": "string" + }, + "exchangeOrderId": { + "description": "Order ID created for this exchange", + "type": "string" + }, + "exchangeOrderName": { + "description": "Order number/name for exchange order", + "type": "string" + }, + "sku": { + "description": "Product Variant SKU", + "type": "string" + }, + "name": { + "description": "Product name", + "type": "string" + }, + "quantity": { + "description": "Quantity requested", + "type": "number", + "minimum": 1 + }, + "unitPrice": { + "description": "Unit price", + "type": "number" + } + }, + "required": [ + "sku", + "quantity" + ], + "additionalProperties": false + } + }, + "totalQuantity": { + "description": "Total quantity of items being returned (excludes exchange items)", + "type": "number" + }, + "returnMethod": { + "type": "object", + "properties": { + "provider": { + "description": "Return logistics provider", + "type": "string" + }, + "methodType": { + "description": "Method customer uses to return items", + "type": "string" + }, + "address": { + "description": "Address where customer returns items", + "type": "object", + "properties": { + "address1": { + "description": "Primary street address (e.g., \"123 Main Street\")", + "type": "string" + }, + "address2": { + "description": "Secondary address information such as apartment, suite, or unit number (e.g., \"Apt 4B\")", + "type": "string" + }, + "city": { + "description": "City or town name", + "type": "string" + }, + "company": { + "description": "Company or organization name associated with this address", + "type": "string" + }, + "country": { + "description": "Country code in ISO 3166-1 alpha-2 format (2 letters, e.g., \"US\", \"CA\", \"GB\")", + "type": "string" + }, + "email": { + "description": "Email address for contact at this location", + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + }, + "firstName": { + "description": "First name of the person at this address", + "type": "string" + }, + "lastName": { + "description": "Last name of the person at this address", + "type": "string" + }, + "phone": { + "description": "Phone number including country code if applicable (e.g., \"+1-555-123-4567\")", + "type": "string" + }, + "stateOrProvince": { + "description": "State or province. For US addresses, use 2-letter state code (e.g., \"CA\", \"NY\"). For other countries, use full province name or local standard.", + "type": "string" + }, + "zipCodeOrPostalCode": { + "description": "ZIP code (US) or postal code (international) for the address", + "type": "string" + } + }, + "additionalProperties": false + }, + "qrCodeUrl": { + "description": "QR code URL for label-free return methods", + "type": "string", + "format": "uri" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + } + }, + "additionalProperties": false + }, + "returnShippingAddress": { + "description": "Address where items should be returned to", + "type": "object", + "properties": { + "address1": { + "description": "Primary street address (e.g., \"123 Main Street\")", + "type": "string" + }, + "address2": { + "description": "Secondary address information such as apartment, suite, or unit number (e.g., \"Apt 4B\")", + "type": "string" + }, + "city": { + "description": "City or town name", + "type": "string" + }, + "company": { + "description": "Company or organization name associated with this address", + "type": "string" + }, + "country": { + "description": "Country code in ISO 3166-1 alpha-2 format (2 letters, e.g., \"US\", \"CA\", \"GB\")", + "type": "string" + }, + "email": { + "description": "Email address for contact at this location", + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + }, + "firstName": { + "description": "First name of the person at this address", + "type": "string" + }, + "lastName": { + "description": "Last name of the person at this address", + "type": "string" + }, + "phone": { + "description": "Phone number including country code if applicable (e.g., \"+1-555-123-4567\")", + "type": "string" + }, + "stateOrProvince": { + "description": "State or province. For US addresses, use 2-letter state code (e.g., \"CA\", \"NY\"). For other countries, use full province name or local standard.", + "type": "string" + }, + "zipCodeOrPostalCode": { + "description": "ZIP code (US) or postal code (international) for the address", + "type": "string" + } + }, + "additionalProperties": false + }, + "labels": { + "description": "Shipping labels for this return", + "type": "array", + "items": { + "type": "object", + "properties": { + "status": { + "description": "Label lifecycle status", + "type": "string" + }, + "carrier": { + "description": "Shipping carrier providing the label", + "type": "string" + }, + "trackingNumber": { + "description": "Tracking number for the return shipment", + "type": "string" + }, + "url": { + "description": "URL to download the shipping label", + "type": "string", + "format": "uri" + }, + "rate": { + "description": "Shipping cost for this label", + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + } + }, + "required": [ + "carrier", + "trackingNumber" + ], + "additionalProperties": false + } + }, + "locationId": { + "description": "Warehouse facility identifier where return will be received", + "type": "string" + }, + "returnTotal": { + "description": "Gross merchandise value of returned items before fees", + "type": "number" + }, + "exchangeTotal": { + "description": "Gross merchandise value of exchange items before any credits applied", + "type": "number" + }, + "refundAmount": { + "description": "Final refund amount to customer after fees and restocking charges", + "type": "number" + }, + "refundMethod": { + "description": "Payment method for issuing the refund", + "type": "string" + }, + "refundStatus": { + "description": "Payment refund processing status (separate from return status)", + "type": "string" + }, + "refundTransactionId": { + "description": "Transaction ID for the refund", + "type": "string" + }, + "shippingRefundAmount": { + "description": "Amount of original shipping cost being refunded", + "type": "number" + }, + "returnShippingFees": { + "description": "Return shipping cost charged to customer (if applicable)", + "type": "number" + }, + "restockingFee": { + "description": "Total restocking fees charged to customer across all items", + "type": "number" + }, + "requestedAt": { + "description": "When return was requested", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "receivedAt": { + "description": "When returned items were received", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "completedAt": { + "description": "When return was fully processed", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "customerNote": { + "description": "Customer notes about the return", + "type": "string" + }, + "internalNote": { + "description": "Internal notes for staff", + "type": "string" + }, + "returnInstructions": { + "description": "Instructions provided to customer", + "type": "string" + }, + "declineReason": { + "description": "Reason if return was declined", + "type": "string" + }, + "statusPageUrl": { + "description": "Customer-facing status tracking page", + "type": "string", + "format": "uri" + }, + "tags": { + "description": "Tags for categorization and filtering. Useful for organizing entities with custom labels (e.g., \"priority\", \"wholesale\", \"gift\")", + "type": "array", + "items": { + "type": "string" + } + }, + "customFields": { + "description": "Custom Fields - allows for arbitrary key-value pairs to be added to an entity. Useful for storing any custom data that is not covered by the other fields.", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "additionalProperties": false + } + } + }, + "required": [ + "orderId", + "outcome", + "returnLineItems" + ], + "additionalProperties": false + } + }, + "required": [ + "return" + ], + "additionalProperties": false +} diff --git a/schemas/tool-inputs/get-product-variants.json b/schemas/tool-inputs/get-product-variants.json new file mode 100644 index 0000000..ced2ee7 --- /dev/null +++ b/schemas/tool-inputs/get-product-variants.json @@ -0,0 +1,68 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "get-product-variants", + "description": "Input schema for querying product variants", + "type": "object", + "properties": { + "ids": { + "description": "Unique variant IDs in the fulfillment system", + "type": "array", + "items": { + "type": "string" + } + }, + "skus": { + "description": "Variant SKUs (Stock Keeping Units)", + "type": "array", + "items": { + "type": "string" + } + }, + "productIds": { + "description": "Parent product IDs; returns all variants under each product", + "type": "array", + "items": { + "type": "string" + } + }, + "updatedAtMin": { + "description": "Minimum updated at date (inclusive)", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "updatedAtMax": { + "description": "Maximum updated at date (inclusive)", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "createdAtMin": { + "description": "Minimum created at date (inclusive)", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "createdAtMax": { + "description": "Maximum created at date (inclusive)", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "pageSize": { + "description": "Number of results to return per page. Use with skip to paginate through results.", + "default": 10, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "skip": { + "description": "Number of results to skip. To navigate to the next page, increment skip by pageSize (e.g., skip=0 for first page, skip=100 for second page when pageSize=100).", + "default": 0, + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "additionalProperties": false +} diff --git a/schemas/tool-inputs/get-returns.json b/schemas/tool-inputs/get-returns.json new file mode 100644 index 0000000..51761a8 --- /dev/null +++ b/schemas/tool-inputs/get-returns.json @@ -0,0 +1,82 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "get-returns", + "description": "Input schema for querying returns", + "type": "object", + "properties": { + "ids": { + "description": "Internal return IDs", + "type": "array", + "items": { + "type": "string" + } + }, + "orderIds": { + "description": "Order IDs to find returns for", + "type": "array", + "items": { + "type": "string" + } + }, + "returnNumbers": { + "description": "Return numbers (customer-facing identifiers)", + "type": "array", + "items": { + "type": "string" + } + }, + "statuses": { + "description": "Return statuses", + "type": "array", + "items": { + "type": "string" + } + }, + "outcomes": { + "description": "Return outcomes (refund/exchange)", + "type": "array", + "items": { + "type": "string" + } + }, + "updatedAtMin": { + "description": "Minimum updated at date (inclusive)", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "updatedAtMax": { + "description": "Maximum updated at date (inclusive)", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "createdAtMin": { + "description": "Minimum created at date (inclusive)", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "createdAtMax": { + "description": "Maximum created at date (inclusive)", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "pageSize": { + "description": "Number of results to return per page. Use with skip to paginate through results.", + "default": 10, + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "skip": { + "description": "Number of results to skip. To navigate to the next page, increment skip by pageSize (e.g., skip=0 for first page, skip=100 for second page when pageSize=100).", + "default": 0, + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + } + }, + "additionalProperties": false +} diff --git a/validator/CLAUDE.md b/validator/CLAUDE.md new file mode 100644 index 0000000..072f204 --- /dev/null +++ b/validator/CLAUDE.md @@ -0,0 +1,37 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## CRITICAL RULES + +### NEVER touch the upstream repository +**NEVER interact with `commerce-operations-foundation/mcp-reference-server` for any reason.** +- Do NOT push to upstream +- Do NOT create PRs targeting upstream +- Do NOT use `gh` commands that reference `commerce-operations-foundation` +- Only push to `origin` (`cghobson/mcp-reference-server`) +- Only create PRs within `cghobson/mcp-reference-server` (base and head both on origin) + +### Git workflow +- Push branches to `origin` only +- When creating PRs, both base and head must be on `cghobson/mcp-reference-server` +- Never use `--repo commerce-operations-foundation/mcp-reference-server` with any gh command +- **ALWAYS run `git status` before staging**. Never use `git add -A` or `git add .` without first checking what will be staged + +## Code Principles + +Before writing any line of code, ask: **"Is this necessary, or just present?"** + +- **No unnecessary re-exports**: If consumers can import directly from the source, don't re-export through intermediate modules +- **No wrapper functions that just call another function**: If `foo()` just returns `bar()`, delete `foo()` and have callers use `bar()` directly +- **No intermediate layers that add no value**: Barrel files, facade modules, and adapter layers must justify their existence +- **Don't duplicate what's already defined elsewhere**: If behavior is specified in schema files (JSON), derive it programmatically rather than hardcoding it in TypeScript +- **Delete unused code**: If nothing calls it, remove it + +## Project Structure + +This is a monorepo containing: +- `server/` - MCP server for fulfillment/commerce operations +- `validator/` - MCP validator tool + +See `server/CLAUDE.md` for server-specific guidance. diff --git a/validator/README.md b/validator/README.md new file mode 100644 index 0000000..20ba5f3 --- /dev/null +++ b/validator/README.md @@ -0,0 +1,195 @@ +# onX MCP Validator + +Compliance validator for Commerce Operations Foundation (COF) onX MCP servers. + +## Installation + +```bash +npm install -g @cof-org/onx-validator +``` + +Or run directly with npx: + +```bash +npx @cof-org/onx-validator stdio node ./path/to/your/server.js +``` + +## Usage + +### Validate a stdio MCP Server + +```bash +# Basic validation +onx-validate stdio node ./dist/index.js + +# With environment variables +onx-validate stdio node -a "./dist/index.js" -e ADAPTER_TYPE=built-in -e ADAPTER_NAME=mock + +# JSON output (for CI/CD) +onx-validate stdio node -a "./dist/index.js" -f json + +# Skip functional tests (schema validation only) +onx-validate stdio node -a "./dist/index.js" --no-functional + +# Verbose output +onx-validate stdio node -a "./dist/index.js" -v +``` + +### Quick Health Check + +```bash +onx-validate check node ./dist/index.js +``` + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Fully compliant | +| 1 | Partially compliant | +| 2 | Non-compliant | +| 3 | Validation error (server crash, connection failed, etc.) | + +## Required Tools + +The 1.0.0 onX specification requires these tools: + +### Action Tools +- `create-sales-order` - Create new orders +- `update-order` - Modify existing orders +- `cancel-order` - Cancel orders +- `fulfill-order` - Mark orders as fulfilled +- `create-return` - Process returns + +### Query Tools +- `get-orders` - Retrieve orders +- `get-customers` - Retrieve customers +- `get-products` - Retrieve products +- `get-product-variants` - Retrieve product variants +- `get-inventory` - Check inventory levels +- `get-fulfillments` - Retrieve fulfillment records +- `get-returns` - Retrieve return records + +## Validation Levels + +### 1. Tool Presence +Verifies all required tools are implemented. + +### 2. Schema Validation +Checks that tool input schemas match the spec: +- Required properties are present and marked required +- Property types are correct +- Nested structures (e.g., `order.lineItems`) have correct required fields + +### 3. Functional Tests +Calls tools with test data to verify: +- Required field validation works (rejects invalid inputs) +- Valid inputs are accepted +- Response format is correct + +## Compliance Report + +``` +═══════════════════════════════════════════════════════════════ + onX MCP Compliance Report +═══════════════════════════════════════════════════════════════ + + Server: my-server v1.0.0 + Protocol: 2024-11-05 + Transport: stdio + Timestamp: 2026-01-26T12:00:00.000Z + +─── Summary ───────────────────────────────────────────────────── + + Status: ● FULLY COMPLIANT + Score: ████████████████████ 100% + + Tools Implemented: 12/12 + Checks Passed: 12 + Checks Failed: 0 + Warnings: 0 + +─── Tool Details ──────────────────────────────────────────────── + + ✓ create-sales-order + ✓ update-order + ✓ cancel-order + ... +═══════════════════════════════════════════════════════════════ + + ✓ This server is fully compliant with the onX MCP specification. +``` + +## JSON Output + +Use `-f json` for machine-readable output: + +```json +{ + "serverInfo": { + "name": "my-server", + "version": "1.0.0", + "protocolVersion": "2024-11-05" + }, + "timestamp": "2026-01-26T12:00:00.000Z", + "transport": "stdio", + "summary": { + "totalTools": 12, + "implementedTools": 12, + "missingTools": [], + "passedChecks": 12, + "failedChecks": 0, + "warnings": 0 + }, + "tools": [...], + "compliance": "full", + "score": 100 +} +``` + +## Programmatic Usage + +```typescript +import { OnxValidator } from '@cof-org/onx-validator'; + +const validator = OnxValidator.forStdio({ + type: 'stdio', + command: 'node', + args: ['./dist/index.js'], + env: { ADAPTER_TYPE: 'built-in', ADAPTER_NAME: 'mock' }, +}); + +const report = await validator.validate(); + +if (report.compliance === 'full') { + console.log('Server is compliant!'); +} else { + console.log('Issues found:', report.summary.failedChecks); +} +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: Validate MCP Server + +on: [push, pull_request] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npm ci + - run: npm run build + - run: npx @cof-org/onx-validator stdio node -a "./dist/index.js" -e ADAPTER_TYPE=built-in +``` + +## License + +MIT diff --git a/validator/action/README.md b/validator/action/README.md new file mode 100644 index 0000000..85e06fe --- /dev/null +++ b/validator/action/README.md @@ -0,0 +1,26 @@ +# onX MCP Validator GitHub Action + +Validate MCP servers against the onX specification in CI/CD. + +## Usage + +```yaml +- name: Validate MCP Server + uses: commerce-operations-foundation/onx-validator-action@v1 + with: + server-command: node + server-args: ./dist/index.js + env-vars: | + ADAPTER_TYPE=built-in + ADAPTER_NAME=mock +``` + +## Inputs/Outputs + +See [action.yml](./action.yml) for all inputs and outputs. + +## Exit Behavior + +- **Full compliance**: Passes +- **Partial compliance**: Warning (or fails with `fail-on-partial: true`) +- **Non-compliant**: Fails diff --git a/validator/action/action.yml b/validator/action/action.yml new file mode 100644 index 0000000..93427fc --- /dev/null +++ b/validator/action/action.yml @@ -0,0 +1,134 @@ +name: 'onX MCP Validator' +description: 'Validate MCP servers against the Commerce Operations Foundation onX specification' +author: 'Commerce Operations Foundation' + +branding: + icon: 'check-circle' + color: 'green' + +inputs: + server-command: + description: 'Command to start the MCP server' + required: true + server-args: + description: 'Arguments to pass to the server (space-separated)' + required: false + default: '' + env-vars: + description: 'Environment variables (KEY=VALUE, one per line)' + required: false + default: '' + functional-tests: + description: 'Run functional tests (true/false)' + required: false + default: 'true' + fail-on-partial: + description: 'Fail if only partially compliant (true/false)' + required: false + default: 'false' + working-directory: + description: 'Working directory for running the server' + required: false + default: '.' + +outputs: + compliance: + description: 'Compliance status (full, partial, non-compliant)' + score: + description: 'Compliance score (0-100)' + report: + description: 'Full JSON compliance report' + implemented-tools: + description: 'Number of implemented tools' + missing-tools: + description: 'Comma-separated list of missing tools' + +runs: + using: 'composite' + steps: + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install validator + shell: bash + run: npm install -g @cof-org/onx-validator + + - name: Parse environment variables + id: parse-env + shell: bash + run: | + ENV_ARGS="" + if [ -n "${{ inputs.env-vars }}" ]; then + while IFS= read -r line; do + if [ -n "$line" ]; then + ENV_ARGS="$ENV_ARGS -e $line" + fi + done <<< "${{ inputs.env-vars }}" + fi + echo "env_args=$ENV_ARGS" >> $GITHUB_OUTPUT + + - name: Run validation + id: validate + shell: bash + working-directory: ${{ inputs.working-directory }} + run: | + # Build args + ARGS="" + if [ -n "${{ inputs.server-args }}" ]; then + for arg in ${{ inputs.server-args }}; do + ARGS="$ARGS -a $arg" + done + fi + + # Run validator + FUNCTIONAL_FLAG="" + if [ "${{ inputs.functional-tests }}" = "false" ]; then + FUNCTIONAL_FLAG="--no-functional" + fi + + set +e + REPORT=$(onx-validate stdio "${{ inputs.server-command }}" $ARGS ${{ steps.parse-env.outputs.env_args }} $FUNCTIONAL_FLAG -f json 2>&1) + EXIT_CODE=$? + set -e + + # Extract values from JSON + COMPLIANCE=$(echo "$REPORT" | jq -r '.compliance // "error"') + SCORE=$(echo "$REPORT" | jq -r '.score // 0') + IMPLEMENTED=$(echo "$REPORT" | jq -r '.summary.implementedTools // 0') + MISSING=$(echo "$REPORT" | jq -r '.summary.missingTools | join(",")') + + # Set outputs + echo "compliance=$COMPLIANCE" >> $GITHUB_OUTPUT + echo "score=$SCORE" >> $GITHUB_OUTPUT + echo "implemented-tools=$IMPLEMENTED" >> $GITHUB_OUTPUT + echo "missing-tools=$MISSING" >> $GITHUB_OUTPUT + + # Store full report (escape for multiline) + EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) + echo "report<<$EOF" >> $GITHUB_OUTPUT + echo "$REPORT" >> $GITHUB_OUTPUT + echo "$EOF" >> $GITHUB_OUTPUT + + # Print report to console + if [ "$COMPLIANCE" != "error" ]; then + onx-validate stdio "${{ inputs.server-command }}" $ARGS ${{ steps.parse-env.outputs.env_args }} $FUNCTIONAL_FLAG || true + else + echo "::error::Validation failed" + echo "$REPORT" + fi + + # Determine exit code + if [ "$COMPLIANCE" = "full" ]; then + exit 0 + elif [ "$COMPLIANCE" = "partial" ] && [ "${{ inputs.fail-on-partial }}" = "true" ]; then + echo "::error::Server is only partially compliant" + exit 1 + elif [ "$COMPLIANCE" = "partial" ]; then + echo "::warning::Server is only partially compliant" + exit 0 + else + echo "::error::Server is non-compliant" + exit 1 + fi diff --git a/validator/package-lock.json b/validator/package-lock.json new file mode 100644 index 0000000..ff570d6 --- /dev/null +++ b/validator/package-lock.json @@ -0,0 +1,2487 @@ +{ + "name": "@cof-org/onx-validator", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@cof-org/onx-validator", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@onx/schemas": "file:../schemas", + "commander": "^12.0.0" + }, + "bin": { + "onx-validate": "dist/cli/index.js" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "@vitest/coverage-v8": "^3.2.4", + "prettier": "^3.1.1", + "typescript": "^5.3.3", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "../schemas": { + "name": "@onx/schemas", + "version": "1.0.0" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@onx/schemas": { + "resolved": "../schemas", + "link": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/validator/package.json b/validator/package.json new file mode 100644 index 0000000..3e10ed2 --- /dev/null +++ b/validator/package.json @@ -0,0 +1,62 @@ +{ + "name": "@cof-org/onx-validator", + "version": "1.0.0", + "description": "Compliance validator for Commerce Operations Foundation onX MCP servers", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "onx-validate": "./dist/cli/index.js" + }, + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "start": "node dist/cli/index.js", + "test": "vitest", + "test:watch": "vitest --watch", + "lint": "eslint src --ext .ts", + "format": "prettier --write \"src/**/*.ts\"", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist", + "prepublishOnly": "npm run clean && npm run build" + }, + "keywords": [ + "mcp", + "validator", + "compliance", + "commerce", + "fulfillment", + "onx" + ], + "author": "Commerce Operations Foundation", + "license": "MIT", + "dependencies": { + "@onx/schemas": "file:../schemas", + "commander": "^12.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.5", + "@vitest/coverage-v8": "^3.2.4", + "prettier": "^3.1.1", + "typescript": "^5.3.3", + "vitest": "^3.2.4" + }, + "engines": { + "node": ">=18.0.0" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "https://github.com/commerce-operations-foundation/mcp-reference-server.git" + } +} diff --git a/validator/src/cli/index.ts b/validator/src/cli/index.ts new file mode 100644 index 0000000..08258ed --- /dev/null +++ b/validator/src/cli/index.ts @@ -0,0 +1,108 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import { OnxValidator } from '../validator.js'; +import { ComplianceReport } from '../types.js'; + +function exitWithCompliance(report: ComplianceReport): never { + if (report.compliance === 'full') { + process.exit(0); + } else if (report.compliance === 'partial') { + process.exit(1); + } else { + process.exit(2); + } +} + +const program = new Command(); + +program + .name('onx-validate') + .description('Validate MCP servers against the onX specification') + .version('1.0.0'); + +program + .command('stdio') + .description('Validate a stdio-based MCP server') + .argument('', 'Command to start the MCP server') + .option('-a, --args ', 'Arguments to pass to the server command') + .option('-e, --env ', 'Environment variables (KEY=VALUE)') + .option('--no-functional', 'Skip functional tests') + .option('-f, --format ', 'Output format (console, json)', 'console') + .option('-v, --verbose', 'Verbose output') + .action(async (command: string, options: { + args?: string[]; + env?: string[]; + functional: boolean; + format: string; + verbose?: boolean; + }) => { + const env: Record = {}; + if (options.env) { + for (const envVar of options.env) { + const [key, ...valueParts] = envVar.split('='); + if (key && valueParts.length > 0) { + env[key] = valueParts.join('='); + } + } + } + + const validator = OnxValidator.create( + { type: 'stdio', command, args: options.args, env }, + { functionalTests: options.functional, format: options.format as 'console' | 'json', verbose: options.verbose } + ); + + try { + const report = await validator.run(); + exitWithCompliance(report); + } catch (error) { + console.error('Validation failed:', error instanceof Error ? error.message : error); + process.exit(3); + } + }); + +program + .command('http') + .description('Validate an HTTP-based MCP server') + .argument('', 'URL of the MCP server endpoint') + .option('-H, --header ', 'HTTP headers (Header: Value)') + .option('--no-functional', 'Skip functional tests') + .option('-f, --format ', 'Output format (console, json)', 'console') + .option('-v, --verbose', 'Verbose output') + .action(async (url: string, options: { + header?: string[]; + functional: boolean; + format: string; + verbose?: boolean; + }) => { + const headers: Record = {}; + if (options.header) { + for (const header of options.header) { + const colonIndex = header.indexOf(':'); + if (colonIndex > 0) { + const key = header.substring(0, colonIndex).trim(); + const value = header.substring(colonIndex + 1).trim(); + headers[key] = value; + } + } + } + + const validator = OnxValidator.create( + { type: 'http', url, headers: Object.keys(headers).length > 0 ? headers : undefined }, + { functionalTests: options.functional, format: options.format as 'console' | 'json', verbose: options.verbose } + ); + + try { + const report = await validator.run(); + exitWithCompliance(report); + } catch (error) { + console.error('Validation failed:', error instanceof Error ? error.message : error); + process.exit(3); + } + }); + +if (process.argv.length < 3) { + program.help(); +} + +program.parse(); diff --git a/validator/src/index.ts b/validator/src/index.ts new file mode 100644 index 0000000..114adf5 --- /dev/null +++ b/validator/src/index.ts @@ -0,0 +1,17 @@ +/** + * onX MCP Validator + * Compliance validator for Commerce Operations Foundation onX MCP servers + */ + +export { OnxValidator, ValidatorOptions } from './validator.js'; +export { + ComplianceReport, + ToolValidationResult, + ValidationResult, + StdioTransportConfig, + HttpTransportConfig, + TransportConfig, +} from './types.js'; +export { McpTransport, ServerInfo, McpToolResult } from './transports/index.js'; +export { ToolValidator, FunctionalValidator } from './validators/index.js'; +export { ReportGenerator, ConsoleReporter } from './reporters/index.js'; diff --git a/validator/src/reporters/console-reporter.ts b/validator/src/reporters/console-reporter.ts new file mode 100644 index 0000000..f9bf89c --- /dev/null +++ b/validator/src/reporters/console-reporter.ts @@ -0,0 +1,142 @@ +/** + * Console Reporter + * Formats compliance reports for terminal output + */ + +import { ComplianceReport, ToolValidationResult } from '../types.js'; +import { ONX_TOOLS } from '@onx/schemas'; + +const colors = { + green: (s: string) => `\x1b[32m${s}\x1b[0m`, + red: (s: string) => `\x1b[31m${s}\x1b[0m`, + yellow: (s: string) => `\x1b[33m${s}\x1b[0m`, + blue: (s: string) => `\x1b[34m${s}\x1b[0m`, + cyan: (s: string) => `\x1b[36m${s}\x1b[0m`, + bold: (s: string) => `\x1b[1m${s}\x1b[0m`, + dim: (s: string) => `\x1b[2m${s}\x1b[0m`, +}; + +export class ConsoleReporter { + print(report: ComplianceReport): void { + this.printHeader(report); + this.printSummary(report); + this.printToolResults(report); + this.printFooter(report); + } + + private printHeader(report: ComplianceReport): void { + console.log(); + console.log(colors.bold('═══════════════════════════════════════════════════════════════')); + console.log(colors.bold(' onX MCP Compliance Report ')); + console.log(colors.bold('═══════════════════════════════════════════════════════════════')); + console.log(); + console.log(` Server: ${colors.cyan(report.serverInfo.name)} v${report.serverInfo.version}`); + console.log(` Protocol: ${report.serverInfo.protocolVersion}`); + console.log(` Transport: ${report.transport}`); + console.log(` Timestamp: ${report.timestamp}`); + console.log(); + } + + private printSummary(report: ComplianceReport): void { + const { summary, compliance, score } = report; + + let complianceBadge: string; + switch (compliance) { + case 'full': + complianceBadge = colors.green('● FULLY COMPLIANT'); + break; + case 'partial': + complianceBadge = colors.yellow('◐ PARTIALLY COMPLIANT'); + break; + case 'non-compliant': + complianceBadge = colors.red('○ NON-COMPLIANT'); + break; + } + + console.log(colors.bold('─── Summary ─────────────────────────────────────────────────────')); + console.log(); + console.log(` Status: ${complianceBadge}`); + console.log(` Score: ${this.formatScore(score)}`); + console.log(); + console.log(` Tools on Server: ${summary.totalTools}`); + console.log(` onX Tools Implemented: ${summary.onXToolsImplemented}/${summary.onXTools}`); + console.log(` onX Tools Missing: ${summary.missingTools > 0 ? colors.red(String(summary.missingTools)) : '0'}`); + console.log(` Other Tools: ${summary.otherTools}`); + console.log(` Checks Passed: ${colors.green(String(summary.passedChecks))}`); + console.log(` Checks Failed: ${summary.failedChecks > 0 ? colors.red(String(summary.failedChecks)) : '0'}`); + console.log(` Warnings: ${summary.warnings > 0 ? colors.yellow(String(summary.warnings)) : '0'}`); + + console.log(); + } + + private printToolResults(report: ComplianceReport): void { + console.log(colors.bold('─── Tool Details ────────────────────────────────────────────────')); + console.log(); + + for (const toolName of ONX_TOOLS) { + const result = report.tools.find(t => t.tool === toolName); + this.printToolResult(toolName, result); + } + + const extraTools = report.tools.filter( + t => !ONX_TOOLS.includes(t.tool as any) + ); + if (extraTools.length > 0) { + console.log(colors.dim(' Additional Tools:')); + for (const tool of extraTools) { + console.log(` ${colors.dim('○')} ${colors.dim(tool.tool)}`); + } + console.log(); + } + } + + private printToolResult(toolName: string, result?: ToolValidationResult): void { + if (!result || !result.exists) { + console.log(` ${colors.red('✗')} ${toolName} ${colors.red('(missing)')}`); + return; + } + + const allPassed = result.schemaValid && result.functionalValid && result.errors.length === 0; + const icon = allPassed ? colors.green('✓') : colors.yellow('◐'); + const status = allPassed ? '' : colors.yellow(`(${result.errors.length} issues)`); + + console.log(` ${icon} ${toolName} ${status}`); + + for (const error of result.errors.filter(e => !e.passed)) { + console.log(` ${colors.red('└─')} ${error.message}`); + } + + for (const warning of result.warnings) { + console.log(` ${colors.yellow('└─')} ${warning.message}`); + } + } + + private printFooter(report: ComplianceReport): void { + console.log(colors.bold('═══════════════════════════════════════════════════════════════')); + + if (report.compliance === 'full') { + console.log(); + console.log(colors.green(' ✓ This server is fully compliant with the onX MCP specification.')); + console.log(); + } else { + console.log(); + console.log(colors.yellow(' ⚠ This server has compliance issues. See details above.')); + console.log(); + } + } + + private formatScore(score: number): string { + const bar = this.createProgressBar(score, 20); + let color = colors.green; + if (score < 50) color = colors.red; + else if (score < 80) color = colors.yellow; + + return `${color(bar)} ${score}%`; + } + + private createProgressBar(percent: number, width: number): string { + const filled = Math.round((percent / 100) * width); + const empty = width - filled; + return '█'.repeat(filled) + '░'.repeat(empty); + } +} diff --git a/validator/src/reporters/index.ts b/validator/src/reporters/index.ts new file mode 100644 index 0000000..4ee4b33 --- /dev/null +++ b/validator/src/reporters/index.ts @@ -0,0 +1,2 @@ +export { ReportGenerator } from './report-generator.js'; +export { ConsoleReporter } from './console-reporter.js'; diff --git a/validator/src/reporters/report-generator.ts b/validator/src/reporters/report-generator.ts new file mode 100644 index 0000000..b94eb43 --- /dev/null +++ b/validator/src/reporters/report-generator.ts @@ -0,0 +1,104 @@ +/** + * Report Generator + * Generates compliance reports from validation results + */ + +import { + ComplianceReport, + ToolValidationResult, + ValidationResult, +} from '../types.js'; +import { ONX_TOOLS } from '@onx/schemas'; +import { ServerInfo } from '../transports/index.js'; + +export class ReportGenerator { + generate( + serverInfo: ServerInfo, + toolResults: ToolValidationResult[], + functionalResults: Map, + transport: 'stdio' | 'http' + ): ComplianceReport { + for (const toolResult of toolResults) { + const funcResults = functionalResults.get(toolResult.tool); + if (funcResults) { + const funcErrors = funcResults.filter(r => !r.passed); + toolResult.errors.push(...funcErrors); + toolResult.functionalValid = funcErrors.length === 0; + } + } + + // All tools on server that exist + const serverTools = toolResults.filter(t => t.exists); + + // onX standard tools that exist on the server + const onxImplementedTools = toolResults.filter( + t => t.exists && ONX_TOOLS.includes(t.tool as typeof ONX_TOOLS[number]) + ); + + // Non-onX tools on server (other tools) + const otherTools = toolResults.filter( + t => t.exists && !ONX_TOOLS.includes(t.tool as typeof ONX_TOOLS[number]) + ); + + // Missing onX standard tools (count) + const missingToolsCount = ONX_TOOLS.length - onxImplementedTools.length; + + let passedChecks = 0; + let failedChecks = 0; + let warnings = 0; + + for (const toolResult of toolResults) { + const isOnxTool = ONX_TOOLS.includes(toolResult.tool as typeof ONX_TOOLS[number]); + + if (isOnxTool) { + // Only count pass/fail for onX standard tools + passedChecks += toolResult.errors.filter(e => e.passed).length; + failedChecks += toolResult.errors.filter(e => !e.passed).length; + + if (toolResult.exists) { + passedChecks++; // Tool presence check passed + } else { + failedChecks++; // Tool presence check failed + } + } + + // Count warnings for all tools + warnings += toolResult.warnings.length; + } + + const totalChecks = passedChecks + failedChecks; + const score = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0; + + let compliance: 'full' | 'partial' | 'non-compliant'; + if (missingToolsCount === 0 && failedChecks === 0) { + compliance = 'full'; + } else if (onxImplementedTools.length >= 1) { + compliance = 'partial'; + } else { + compliance = 'non-compliant'; + } + + return { + serverInfo, + timestamp: new Date().toISOString(), + transport, + summary: { + totalTools: serverTools.length, + otherTools: otherTools.length, + onXTools: ONX_TOOLS.length, + onXToolsImplemented: onxImplementedTools.length, + missingTools: missingToolsCount, + passedChecks, + failedChecks, + warnings, + }, + tools: toolResults, + compliance, + score, + }; + } + + static toJSON(report: ComplianceReport): string { + return JSON.stringify(report, null, 2); + } +} diff --git a/validator/src/schemas/tool-inputs.d.ts b/validator/src/schemas/tool-inputs.d.ts new file mode 100644 index 0000000..e15a38e --- /dev/null +++ b/validator/src/schemas/tool-inputs.d.ts @@ -0,0 +1,8 @@ +/** + * Type declarations for the @onx/schemas package + */ + +declare module '@onx/schemas' { + export const ONX_TOOLS: readonly string[]; + export function getToolInputSchema(toolName: string): unknown | null; +} diff --git a/validator/src/transports/base.ts b/validator/src/transports/base.ts new file mode 100644 index 0000000..8ccde7b --- /dev/null +++ b/validator/src/transports/base.ts @@ -0,0 +1,55 @@ +/** + * Base transport interface for MCP communication + */ + +import { ToolDefinition } from '../types.js'; + +export interface McpToolResult { + content: Array<{ type: string; text?: string }>; + isError: boolean; +} + +export interface ServerInfo { + name: string; + version: string; + protocolVersion: string; +} + +export interface McpTransport { + + // Connect to the MCP server + connect(): Promise; + + // Disconnect from the MCP server + disconnect(): Promise; + + // Get server information via initialize + getServerInfo(): Promise; + + // List all available tools + listTools(): Promise; + + // Call a tool with the given arguments + callTool(name: string, args: Record): Promise; + + // Check if connected + isConnected(): boolean; +} + +export interface JsonRpcRequest { + jsonrpc: '2.0'; + id: number; + method: string; + params?: Record; +} + +export interface JsonRpcResponse { + jsonrpc: '2.0'; + id: number; + result?: unknown; + error?: { + code: number; + message: string; + data?: unknown; + }; +} \ No newline at end of file diff --git a/validator/src/transports/http.ts b/validator/src/transports/http.ts new file mode 100644 index 0000000..550785f --- /dev/null +++ b/validator/src/transports/http.ts @@ -0,0 +1,149 @@ +/** + * HTTP transport for MCP servers exposing HTTP endpoints + */ + +import { McpTransport, McpToolResult, ServerInfo, JsonRpcRequest, JsonRpcResponse } from './base.js'; +import { ToolDefinition, HttpTransportConfig } from '../types.js'; + + +export class HttpTransport implements McpTransport { + private config: HttpTransportConfig; + private requestId = 0; + private serverInfo: ServerInfo | null = null; + private connected = false; + + constructor(config: HttpTransportConfig) { + this.config = config; + } + + async connect(): Promise { + // Send initialize request + const response = await this.sendRequest('initialize', { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { + name: 'onx-validator', + version: '1.0.0', + }, + }); + + if (response.error) { + throw new Error(`Initialize failed: ${response.error.message}`); + } + + const result = response.result as { + serverInfo?: { name?: string; version?: string }; + protocolVersion?: string; + }; + + this.serverInfo = { + name: result.serverInfo?.name || 'unknown', + version: result.serverInfo?.version || 'unknown', + protocolVersion: result.protocolVersion || 'unknown', + }; + + // Send initialized notification + await this.sendNotification('notifications/initialized', {}); + + this.connected = true; + } + + async disconnect(): Promise { + this.connected = false; + this.serverInfo = null; + } + + async getServerInfo(): Promise { + if (!this.serverInfo) { + throw new Error('Not connected'); + } + return this.serverInfo; + } + + async listTools(): Promise { + const response = await this.sendRequest('tools/list', {}); + + if (response.error) { + throw new Error(`Failed to list tools: ${response.error.message}`); + } + + const result = response.result as { tools?: ToolDefinition[] }; + return result.tools || []; + } + + async callTool(name: string, args: Record): Promise { + const response = await this.sendRequest('tools/call', { + name, + arguments: args, + }); + + if (response.error) { + // JSON-RPC level error - wrap as MCP tool result + return { + content: [{ type: 'text', text: response.error.message }], + isError: true, + }; + } + + const result = response.result as { + content?: Array<{ type: string; text?: string }>; + isError?: boolean; + }; + + return { + content: result.content || [], + isError: result.isError ?? false, + }; + } + + isConnected(): boolean { + return this.connected; + } + + private async sendRequest(method: string, params: Record): Promise { + const id = ++this.requestId; + const request: JsonRpcRequest = { + jsonrpc: '2.0', + id, + method, + params, + }; + + const response = await fetch(this.config.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...this.config.headers, + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + throw new Error(`HTTP error: ${response.status} ${response.statusText}`); + } + + const json = await response.json() as JsonRpcResponse; + return json; + } + + private async sendNotification(method: string, params: Record): Promise { + const notification = { + jsonrpc: '2.0', + method, + params, + }; + + try { + await fetch(this.config.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...this.config.headers, + }, + body: JSON.stringify(notification), + }); + } catch { + // Notifications don't require a response, ignore errors + } + } +} diff --git a/validator/src/transports/index.ts b/validator/src/transports/index.ts new file mode 100644 index 0000000..8932832 --- /dev/null +++ b/validator/src/transports/index.ts @@ -0,0 +1,3 @@ +export { McpTransport, McpToolResult, ServerInfo } from './base.js'; +export { StdioTransport } from './stdio.js'; +export { HttpTransport } from './http.js'; diff --git a/validator/src/transports/stdio.ts b/validator/src/transports/stdio.ts new file mode 100644 index 0000000..3064ba7 --- /dev/null +++ b/validator/src/transports/stdio.ts @@ -0,0 +1,219 @@ +/** + * Stdio transport for MCP servers running as child processes + */ + +import { spawn, ChildProcess } from 'node:child_process'; +import { McpTransport, McpToolResult, ServerInfo, JsonRpcRequest, JsonRpcResponse } from './base.js'; +import { ToolDefinition, StdioTransportConfig } from '../types.js'; + +export class StdioTransport implements McpTransport { + private config: StdioTransportConfig; + private process: ChildProcess | null = null; + private requestId = 0; + private pendingRequests = new Map void; + reject: (error: Error) => void; + }>(); + private buffer = ''; + private serverInfo: ServerInfo | null = null; + + constructor(config: StdioTransportConfig) { + this.config = config; + } + + async connect(): Promise { + return new Promise((resolve, reject) => { + this.process = spawn(this.config.command, this.config.args || [], { + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env, ...this.config.env }, + }); + + this.process.stdout?.on('data', (data: Buffer) => { + this.handleData(data.toString()); + }); + + this.process.stderr?.on('data', (data: Buffer) => { + // Log stderr but don't fail - servers may write logs here + if (process.env.DEBUG) { + console.error('[server stderr]', data.toString()); + } + }); + + this.process.on('error', (error) => { + reject(new Error(`Failed to start server: ${error.message}`)); + }); + + this.process.on('close', (code) => { + if (code !== 0 && code !== null) { + // Reject any pending requests + for (const { reject } of this.pendingRequests.values()) { + reject(new Error(`Server process exited with code ${code}`)); + } + this.pendingRequests.clear(); + } + }); + + // Give the process a moment to start, then initialize + setTimeout(async () => { + try { + const response = await this.sendRequest('initialize', { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { + name: 'onx-validator', + version: '1.0.0', + }, + }); + + if (response.error) { + reject(new Error(`Initialize failed: ${response.error.message}`)); + return; + } + + const result = response.result as { + serverInfo?: { name?: string; version?: string }; + protocolVersion?: string; + }; + + this.serverInfo = { + name: result.serverInfo?.name || 'unknown', + version: result.serverInfo?.version || 'unknown', + protocolVersion: result.protocolVersion || 'unknown', + }; + + // Send initialized notification + this.sendNotification('notifications/initialized', {}); + + resolve(); + } catch (error) { + reject(error); + } + }, 500); + }); + } + + async disconnect(): Promise { + if (this.process) { + this.process.kill(); + this.process = null; + } + } + + async getServerInfo(): Promise { + if (!this.serverInfo) { + throw new Error('Not connected'); + } + return this.serverInfo; + } + + async listTools(): Promise { + const response = await this.sendRequest('tools/list', {}); + + if (response.error) { + throw new Error(`Failed to list tools: ${response.error.message}`); + } + + const result = response.result as { tools?: ToolDefinition[] }; + return result.tools || []; + } + + async callTool(name: string, args: Record): Promise { + const response = await this.sendRequest('tools/call', { + name, + arguments: args, + }); + + if (response.error) { + // JSON-RPC level error - wrap as MCP tool result + return { + content: [{ type: 'text', text: response.error.message }], + isError: true, + }; + } + + const result = response.result as { + content?: Array<{ type: string; text?: string }>; + isError?: boolean; + }; + + return { + content: result.content || [], + isError: result.isError ?? false, + }; + } + + isConnected(): boolean { + return this.process !== null && !this.process.killed; + } + + private handleData(data: string): void { + this.buffer += data; + + // Process complete JSON-RPC messages (newline-delimited) + const lines = this.buffer.split('\n'); + this.buffer = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim()) continue; + + try { + const message = JSON.parse(line) as JsonRpcResponse; + + if (message.id !== undefined) { + const pending = this.pendingRequests.get(message.id); + if (pending) { + pending.resolve(message); + this.pendingRequests.delete(message.id); + } + } + } catch { + // Ignore non-JSON lines (might be logs) + if (process.env.DEBUG) { + console.error('[parse error]', line); + } + } + } + } + + private sendRequest(method: string, params: Record): Promise { + return new Promise((resolve, reject) => { + if (!this.process?.stdin) { + reject(new Error('Not connected')); + return; + } + + const id = ++this.requestId; + const request: JsonRpcRequest = { + jsonrpc: '2.0', + id, + method, + params, + }; + + this.pendingRequests.set(id, { resolve, reject }); + + const message = JSON.stringify(request) + '\n'; + this.process.stdin.write(message); + + // Timeout after 30 seconds + setTimeout(() => { + if (this.pendingRequests.has(id)) { + this.pendingRequests.delete(id); + reject(new Error(`Request ${method} timed out`)); + } + }, 30000); + }); + } + + private sendNotification(method: string, params: Record): void { + if (!this.process?.stdin) return; + + const notification = { + jsonrpc: '2.0', + method, + params, + }; + + this.process.stdin.write(JSON.stringify(notification) + '\n'); + } +} diff --git a/validator/src/types.ts b/validator/src/types.ts new file mode 100644 index 0000000..f487ae5 --- /dev/null +++ b/validator/src/types.ts @@ -0,0 +1,90 @@ +/** + * Types for the onX MCP Validator + */ + +export interface ToolDefinition { + name: string; + description?: string; + inputSchema: JSONSchema; +} + +export interface JSONSchema { + type?: string; + properties?: Record; + required?: string[]; + items?: JSONSchema; + additionalProperties?: boolean | JSONSchema; + description?: string; + default?: unknown; + enum?: unknown[]; + minimum?: number; + maximum?: number; + minLength?: number; + maxLength?: number; + pattern?: string; + format?: string; + oneOf?: JSONSchema[]; + anyOf?: JSONSchema[]; + allOf?: JSONSchema[]; + $ref?: string; +} + +export interface ValidationResult { + passed: boolean; + tool: string; + check: string; + message: string; + details?: unknown; +} + +export interface ToolValidationResult { + tool: string; + exists: boolean; + schemaValid: boolean; + functionalValid: boolean; + errors: ValidationResult[]; + warnings: ValidationResult[]; +} + +export interface ComplianceReport { + serverInfo: { + name: string; + version: string; + protocolVersion: string; + }; + timestamp: string; + transport: 'stdio' | 'http'; + summary: { + // Server tools + totalTools: number; // Total tools on server (onX + other) + otherTools: number; // Non-onX tools on server (FYI) + + // onX compliance + onXTools: number; // Total onX standard tools + onXToolsImplemented: number; // onX tools present on server + missingTools: number; // onX tools not present (count) + + // Validation results + passedChecks: number; + failedChecks: number; + warnings: number; + }; + tools: ToolValidationResult[]; + compliance: 'full' | 'partial' | 'non-compliant'; + score: number; // 0-100 +} + +export interface StdioTransportConfig { + type: 'stdio'; + command: string; + args?: string[]; + env?: Record; +} + +export interface HttpTransportConfig { + type: 'http'; + url: string; + headers?: Record; +} + +export type TransportConfig = StdioTransportConfig | HttpTransportConfig; diff --git a/validator/src/validator.ts b/validator/src/validator.ts new file mode 100644 index 0000000..1e4c722 --- /dev/null +++ b/validator/src/validator.ts @@ -0,0 +1,105 @@ +/** + * Main Validator Orchestrator + * Coordinates all validation steps and generates report + */ + +import { McpTransport, StdioTransport, HttpTransport } from './transports/index.js'; +import { ToolValidator, FunctionalValidator } from './validators/index.js'; +import { ReportGenerator, ConsoleReporter } from './reporters/index.js'; +import { ComplianceReport, TransportConfig, ValidationResult } from './types.js'; + +export interface ValidatorOptions { + functionalTests?: boolean; + format?: 'console' | 'json'; + verbose?: boolean; +} + +const defaultOptions: ValidatorOptions = { + functionalTests: true, + format: 'console', + verbose: false, +}; + +export class OnxValidator { + private transport: McpTransport; + private transportType: 'stdio' | 'http'; + private options: ValidatorOptions; + + private constructor(transport: McpTransport, transportType: 'stdio' | 'http', options: ValidatorOptions) { + this.transport = transport; + this.transportType = transportType; + this.options = { ...defaultOptions, ...options }; + } + + static create(config: TransportConfig, options: ValidatorOptions = {}): OnxValidator { + switch (config.type) { + case 'stdio': + return new OnxValidator(new StdioTransport(config), 'stdio', options); + case 'http': + return new OnxValidator(new HttpTransport(config), 'http', options); + } + } + + async validate(): Promise { + const toolValidator = new ToolValidator(); + + try { + if (this.options.verbose) { + console.log('Connecting to MCP server...'); + } + await this.transport.connect(); + + const serverInfo = await this.transport.getServerInfo(); + if (this.options.verbose) { + console.log(`Connected to ${serverInfo.name} v${serverInfo.version}`); + } + + if (this.options.verbose) { + console.log('Fetching tool list...'); + } + const tools = await this.transport.listTools(); + if (this.options.verbose) { + console.log(`Found ${tools.length} tools`); + } + + if (this.options.verbose) { + console.log('Validating tool schemas...'); + } + const toolResults = toolValidator.validateAll(tools); + + let functionalResults = new Map(); + if (this.options.functionalTests) { + if (this.options.verbose) { + console.log('Running functional tests...'); + } + const functionalValidator = new FunctionalValidator(this.transport); + functionalResults = await functionalValidator.runAllTests(); + } + + const reportGenerator = new ReportGenerator(); + const report = reportGenerator.generate( + serverInfo, + toolResults, + functionalResults, + this.transportType + ); + + return report; + } finally { + await this.transport.disconnect(); + } + } + + async run(): Promise { + const report = await this.validate(); + + if (this.options.format === 'json') { + console.log(ReportGenerator.toJSON(report)); + } else { + const consoleReporter = new ConsoleReporter(); + consoleReporter.print(report); + } + + return report; + } +} diff --git a/validator/src/validators/functional-validator.ts b/validator/src/validators/functional-validator.ts new file mode 100644 index 0000000..cc35018 --- /dev/null +++ b/validator/src/validators/functional-validator.ts @@ -0,0 +1,204 @@ +import { McpTransport, McpToolResult } from '../transports/index.js'; +import { ValidationResult, JSONSchema } from '../types.js'; +import { ONX_TOOLS, getToolInputSchema } from '@onx/schemas'; + +interface TestCase { + tool: string; + name: string; + input: Record; + expectIsError?: boolean; +} + +export class FunctionalValidator { + private transport: McpTransport; + + constructor(transport: McpTransport) { + this.transport = transport; + } + + /** + * Run functional tests for all tools + */ + async runAllTests(): Promise> { + const results = new Map(); + + for (const tool of ONX_TOOLS) { + const toolResults = await this.runToolTests(tool); + results.set(tool, toolResults); + } + + return results; + } + + /** + * Run tests for a specific tool + */ + async runToolTests(tool: string): Promise { + const testCases = this.generateTestCases(tool); + const results: ValidationResult[] = []; + + for (const testCase of testCases) { + try { + const response = await this.transport.callTool(testCase.tool, testCase.input); + const validation = this.validateResponse(response, testCase); + results.push(validation); + } catch (error) { + results.push({ + passed: false, + tool: testCase.tool, + check: `functional-${testCase.name}`, + message: `Test threw exception: ${error instanceof Error ? error.message : String(error)}`, + details: { input: testCase.input }, + }); + } + } + + return results; + } + + /** + * Validate an MCP tool response against test expectations + */ + private validateResponse(response: McpToolResult, testCase: TestCase): ValidationResult { + // Check response has required structure + if (!Array.isArray(response.content)) { + return { + passed: false, + tool: testCase.tool, + check: `functional-${testCase.name}`, + message: 'Response missing content array', + details: { input: testCase.input, response }, + }; + } + + if (typeof response.isError !== 'boolean') { + return { + passed: false, + tool: testCase.tool, + check: `functional-${testCase.name}`, + message: 'Response missing isError boolean', + details: { input: testCase.input, response }, + }; + } + + // If we have a specific expectation for isError, validate it + if (testCase.expectIsError !== undefined) { + if (response.isError !== testCase.expectIsError) { + const expected = testCase.expectIsError ? 'error' : 'success'; + const got = response.isError ? 'error' : 'success'; + const errorText = response.content.find(c => c.type === 'text')?.text || ''; + return { + passed: false, + tool: testCase.tool, + check: `functional-${testCase.name}`, + message: `Expected ${expected} but got ${got}${errorText ? `: ${errorText}` : ''}`, + details: { input: testCase.input, response }, + }; + } + } + + // Response is valid + return { + passed: true, + tool: testCase.tool, + check: `functional-${testCase.name}`, + message: `Test "${testCase.name}" passed`, + }; + } + + /** + * Generate test cases automatically from the tool's JSON schema + */ + private generateTestCases(tool: string): TestCase[] { + const schema = getToolInputSchema(tool) as JSONSchema | null; + if (!schema) { + return []; + } + + const testCases: TestCase[] = []; + const required = schema.required || []; + + // Test 1: Empty input - should error if there are required fields + if (required.length > 0) { + testCases.push({ + tool, + name: 'empty-input', + input: {}, + expectIsError: true, + }); + } + + // Test 2: For each required field, test with it missing - should error + for (const field of required) { + const minimalValid = this.buildMinimalValidInput(schema); + delete minimalValid[field]; + + testCases.push({ + tool, + name: `missing-required-${field}`, + input: minimalValid, + expectIsError: true, + }); + } + + // Test 3: Minimal valid input - should get a valid response (don't care about isError) + testCases.push({ + tool, + name: 'schema-valid-input', + input: this.buildMinimalValidInput(schema), + expectIsError: undefined, // Any response is fine - "not found" is valid + }); + + return testCases; + } + + /** + * Build a minimal valid input object based on schema requirements + */ + private buildMinimalValidInput(schema: JSONSchema, path = ''): Record { + const result: Record = {}; + const required = schema.required || []; + const properties = schema.properties || {}; + + for (const field of required) { + const propSchema = properties[field] as JSONSchema | undefined; + if (!propSchema) continue; + + result[field] = this.generateValueForSchema(propSchema, `${path}.${field}`); + } + + return result; + } + + /** + * Generate a valid value for a given schema type + */ + private generateValueForSchema(schema: JSONSchema, path: string): unknown { + switch (schema.type) { + case 'string': + return `test-${path.replace(/\./g, '-')}`; + + case 'number': + case 'integer': + return schema.minimum ?? 1; + + case 'boolean': + return true; + + case 'array': { + const itemSchema = schema.items as JSONSchema | undefined; + if (itemSchema) { + return [this.generateValueForSchema(itemSchema, `${path}[0]`)]; + } + return []; + } + + case 'object': { + return this.buildMinimalValidInput(schema, path); + } + + default: + return null; + } + } +} diff --git a/validator/src/validators/index.ts b/validator/src/validators/index.ts new file mode 100644 index 0000000..7322707 --- /dev/null +++ b/validator/src/validators/index.ts @@ -0,0 +1,2 @@ +export { ToolValidator } from './tool-validator.js'; +export { FunctionalValidator } from './functional-validator.js'; diff --git a/validator/src/validators/schema-comparator.ts b/validator/src/validators/schema-comparator.ts new file mode 100644 index 0000000..4f3036a --- /dev/null +++ b/validator/src/validators/schema-comparator.ts @@ -0,0 +1,196 @@ +/** + * Schema Comparator + * Deep comparison of endpoint schemas against canonical onX schemas + */ + +import { JSONSchema, ValidationResult } from '../types.js'; + +// Compare an endpoint's schema against the canonical schema +// Returns validation results for all differences found +export function compareSchemas( + toolName: string, + endpointSchema: JSONSchema, + canonicalSchema: JSONSchema, + path = '' +): ValidationResult[] { + const results: ValidationResult[] = []; + const currentPath = path || 'root'; + + // Compare type + if (canonicalSchema.type && endpointSchema.type !== canonicalSchema.type) { + results.push({ + passed: false, + tool: toolName, + check: 'schema-type-mismatch', + message: `Type mismatch at ${currentPath}: expected "${canonicalSchema.type}", got "${endpointSchema.type}"`, + details: { path: currentPath, expected: canonicalSchema.type, actual: endpointSchema.type }, + }); + } + + // Compare required fields + const canonicalRequired = new Set(canonicalSchema.required || []); + const endpointRequired = new Set(endpointSchema.required || []); + + // Server missing required field (too lenient) + for (const field of canonicalRequired) { + if (!endpointRequired.has(field)) { + results.push({ + passed: false, + tool: toolName, + check: 'schema-missing-required', + message: `Missing required field "${field}" at ${currentPath}`, + details: { path: currentPath, field }, + }); + } + } + + // Server has extra required field (too strict) + for (const field of endpointRequired) { + if (!canonicalRequired.has(field)) { + results.push({ + passed: false, + tool: toolName, + check: 'schema-extra-required', + message: `Extra required field "${field}" at ${currentPath} (server is stricter than spec)`, + details: { path: currentPath, field }, + }); + } + } + + // Compare properties + if (canonicalSchema.properties) { + const endpointProps = endpointSchema.properties || {}; + const canonicalProps = canonicalSchema.properties; + + for (const [propName, canonicalProp] of Object.entries(canonicalProps)) { + const propPath = path ? `${path}.${propName}` : propName; + const endpointProp = endpointProps[propName]; + + if (!endpointProp) { + results.push({ + passed: false, + tool: toolName, + check: 'schema-missing-property', + message: `Missing property "${propName}" at ${currentPath}`, + details: { path: propPath }, + }); + continue; + } + + // Recursively compare nested schemas + const nestedResults = compareSchemas(toolName, endpointProp, canonicalProp, propPath); + results.push(...nestedResults); + + // Compare constraints + const constraintResults = compareConstraints(toolName, endpointProp, canonicalProp, propPath); + results.push(...constraintResults); + } + + // Check for extra properties when canonical forbids them + if (canonicalSchema.additionalProperties === false) { + for (const propName of Object.keys(endpointProps)) { + if (!(propName in canonicalProps)) { + const propPath = path ? `${path}.${propName}` : propName; + results.push({ + passed: false, + tool: toolName, + check: 'schema-extra-property', + message: `Extra property "${propName}" at ${currentPath} (not allowed by spec)`, + details: { path: propPath }, + }); + } + } + } + } + + // Compare array items + if (canonicalSchema.items && canonicalSchema.type === 'array') { + const itemPath = `${currentPath}[]`; + + if (!endpointSchema.items) { + results.push({ + passed: false, + tool: toolName, + check: 'schema-missing-items', + message: `Missing items schema at ${currentPath}`, + details: { path: itemPath }, + }); + } else { + const itemResults = compareSchemas( + toolName, + endpointSchema.items as JSONSchema, + canonicalSchema.items as JSONSchema, + itemPath + ); + results.push(...itemResults); + } + } + + return results; +} + +type ConstraintKey = 'minimum' | 'maximum' | 'minLength' | 'maxLength' | 'pattern' | 'format'; + +const SIMPLE_CONSTRAINTS: ConstraintKey[] = ['minimum', 'maximum', 'minLength', 'maxLength', 'pattern', 'format']; + +function compareConstraints( + toolName: string, + endpointSchema: JSONSchema, + canonicalSchema: JSONSchema, + path: string +): ValidationResult[] { + const results: ValidationResult[] = []; + + for (const constraint of SIMPLE_CONSTRAINTS) { + const expected = canonicalSchema[constraint]; + if (expected === undefined) continue; + + const actual = endpointSchema[constraint]; + if (actual === undefined) { + results.push({ + passed: false, + tool: toolName, + check: `schema-missing-${constraint}`, + message: `Missing "${constraint}: ${expected}" constraint at ${path}`, + details: { path, constraint, expected }, + }); + } else if (actual !== expected) { + results.push({ + passed: false, + tool: toolName, + check: `schema-${constraint}-mismatch`, + message: `${constraint} mismatch at ${path}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + details: { path, expected, actual }, + }); + } + } + + // Enum requires special handling for set comparison + if (canonicalSchema.enum !== undefined) { + if (endpointSchema.enum === undefined) { + results.push({ + passed: false, + tool: toolName, + check: 'schema-missing-enum', + message: `Missing "enum" constraint at ${path}`, + details: { path, expected: canonicalSchema.enum }, + }); + } else { + const canonicalSet = new Set(canonicalSchema.enum.map(v => JSON.stringify(v))); + const endpointSet = new Set(endpointSchema.enum.map(v => JSON.stringify(v))); + + const missing = [...canonicalSet].filter(v => !endpointSet.has(v)); + if (missing.length > 0) { + results.push({ + passed: false, + tool: toolName, + check: 'schema-enum-mismatch', + message: `Enum mismatch at ${path}: missing values`, + details: { path, missing: missing.map(v => JSON.parse(v)) }, + }); + } + } + } + + return results; +} diff --git a/validator/src/validators/tool-validator.ts b/validator/src/validators/tool-validator.ts new file mode 100644 index 0000000..3f5b049 --- /dev/null +++ b/validator/src/validators/tool-validator.ts @@ -0,0 +1,165 @@ +/** + * Tool Validator + * Validates that tools exist and schemas match canonical onX specifications + */ + +import { + ToolDefinition, + ValidationResult, + ToolValidationResult, + JSONSchema, +} from '../types.js'; +import { ONX_TOOLS, getToolInputSchema } from '@onx/schemas'; +import { compareSchemas } from './schema-comparator.js'; + +export class ToolValidator { + + /** + * Validate all required tools are present + */ + validateToolPresence(tools: ToolDefinition[]): ValidationResult[] { + const results: ValidationResult[] = []; + const toolNames = new Set(tools.map(t => t.name)); + + for (const required of ONX_TOOLS) { + const exists = toolNames.has(required); + results.push({ + passed: exists, + tool: required, + check: 'tool-exists', + message: exists + ? `Tool "${required}" is implemented` + : `Missing required tool: "${required}"`, + }); + } + + return results; + } + + /** + * Validate a single tool's schema against the canonical schema + */ + validateToolSchema(tool: ToolDefinition): ValidationResult[] { + const results: ValidationResult[] = []; + const canonicalSchema = getToolInputSchema(tool.name) as JSONSchema | null; + + // If no canonical schema exists for this tool, it's either: + // 1. Not a required tool (extra tool) - that's fine + // 2. A required tool without a schema file - warn but don't fail + if (!canonicalSchema) { + if (ONX_TOOLS.includes(tool.name as any)) { + results.push({ + passed: true, // Don't fail, just warn + tool: tool.name, + check: 'schema-no-canonical', + message: `No canonical schema found for "${tool.name}" - skipping deep validation`, + }); + } else { + results.push({ + passed: true, + tool: tool.name, + check: 'schema-extra-tool', + message: `Tool "${tool.name}" is not part of onX spec (additional tool)`, + }); + } + return results; + } + + // Check that inputSchema exists + if (!tool.inputSchema || typeof tool.inputSchema !== 'object') { + results.push({ + passed: false, + tool: tool.name, + check: 'schema-exists', + message: `Tool "${tool.name}" is missing inputSchema`, + }); + return results; + } + + // Ensure tool has a description + if (!tool.description || tool.description.trim().length === 0) { + results.push({ + passed: false, + tool: tool.name, + check: 'schema-description', + message: `Tool "${tool.name}" is missing a description`, + }); + } else { + results.push({ + passed: true, + tool: tool.name, + check: 'schema-description', + message: `Tool "${tool.name}" has description`, + }); + } + + // Deep compare endpoint schema against canonical schema + const comparisonResults = compareSchemas(tool.name, tool.inputSchema, canonicalSchema); + results.push(...comparisonResults); + + return results; + } + + /** + * Validate all tools and produce tool-by-tool results + */ + validateAll(tools: ToolDefinition[]): ToolValidationResult[] { + const results: ToolValidationResult[] = []; + const toolMap = new Map(tools.map(t => [t.name, t])); + + for (const requiredName of ONX_TOOLS) { + const tool = toolMap.get(requiredName); + + if (!tool) { + results.push({ + tool: requiredName, + exists: false, + schemaValid: false, + functionalValid: false, + errors: [{ + passed: false, + tool: requiredName, + check: 'tool-exists', + message: `Missing required tool: "${requiredName}"`, + }], + warnings: [], + }); + continue; + } + + const schemaResults = this.validateToolSchema(tool); + const errors = schemaResults.filter(r => !r.passed); + const warnings = schemaResults.filter(r => r.passed && r.check === 'schema-no-canonical'); + + results.push({ + tool: requiredName, + exists: true, + schemaValid: errors.length === 0, + functionalValid: false, // Will be updated by functional tests + errors, + warnings, + }); + } + + // Check for extra tools (not an error, just informational) + for (const tool of tools) { + if (!ONX_TOOLS.includes(tool.name as any)) { + results.push({ + tool: tool.name, + exists: true, + schemaValid: true, + functionalValid: true, + errors: [], + warnings: [{ + passed: true, + tool: tool.name, + check: 'extra-tool', + message: `Additional tool "${tool.name}" found (not part of onX spec)`, + }], + }); + } + } + + return results; + } +} diff --git a/validator/tests/fixtures/index.ts b/validator/tests/fixtures/index.ts new file mode 100644 index 0000000..c84ee2a --- /dev/null +++ b/validator/tests/fixtures/index.ts @@ -0,0 +1,175 @@ +import { vi } from 'vitest'; +import type { ToolDefinition, ValidationResult, ToolValidationResult } from '../../src/types.js'; +import type { McpTransport, McpToolResult, ServerInfo } from '../../src/transports/index.js'; +import { ONX_TOOLS, getToolInputSchema } from '@onx/schemas'; + +export function createServerInfo(overrides: Partial = {}): ServerInfo { + return { + name: 'test-server', + version: '1.0.0', + protocolVersion: '2024-11-05', + ...overrides, + }; +} + +export function createToolDefinition( + name: string, + overrides: Partial = {} +): ToolDefinition { + return { + name, + description: `Test tool: ${name}`, + inputSchema: { + type: 'object', + properties: {}, + }, + ...overrides, + }; +} + +export function createCompliantToolSet(): ToolDefinition[] { + return ONX_TOOLS.map(name => { + const schema = getToolInputSchema(name); + if (!schema) { + throw new Error(`Schema not found for tool: ${name}`); + } + return { + name, + description: schema.description || `Tool: ${name}`, + inputSchema: schema, + }; + }); +} + +/** + * Creates a mock MCP transport for testing + */ +export function createMockTransport(config: { + serverInfo?: ServerInfo; + tools?: ToolDefinition[]; + callToolResponse?: McpToolResult | ((name: string, args: Record) => McpToolResult); +} = {}): McpTransport { + const { + serverInfo = createServerInfo(), + tools = createCompliantToolSet(), + callToolResponse = { content: [{ type: 'text', text: 'OK' }], isError: false }, + } = config; + + let connected = false; + + return { + connect: vi.fn(async () => { + connected = true; + }), + disconnect: vi.fn(async () => { + connected = false; + }), + getServerInfo: vi.fn(async () => { + if (!connected) throw new Error('Not connected'); + return serverInfo; + }), + listTools: vi.fn(async () => { + if (!connected) throw new Error('Not connected'); + return tools; + }), + callTool: vi.fn(async (name: string, args: Record) => { + if (!connected) throw new Error('Not connected'); + if (typeof callToolResponse === 'function') { + return callToolResponse(name, args); + } + return callToolResponse; + }), + isConnected: vi.fn(() => connected), + }; +} + +// Create passing, failing, and tool validation results +export function createPassingResult( + tool: string, + check: string, + message?: string +): ValidationResult { + return { + passed: true, + tool, + check, + message: message || `Check ${check} passed`, + }; +} + +export function createFailingResult( + tool: string, + check: string, + message?: string, + details?: unknown +): ValidationResult { + return { + passed: false, + tool, + check, + message: message || `Check ${check} failed`, + details, + }; +} +export function createToolValidationResult( + tool: string, + config: Partial = {} +): ToolValidationResult { + return { + tool, + exists: true, + schemaValid: true, + functionalValid: true, + errors: [], + warnings: [], + ...config, + }; +} + +/** + * Creates a mock JSON-RPC response + */ +export function createJsonRpcResponse(result: unknown, id = 1) { + return { + jsonrpc: '2.0' as const, + id, + result, + }; +} + +/** + * Creates a mock JSON-RPC error response + */ +export function createJsonRpcError(code: number, message: string, id = 1) { + return { + jsonrpc: '2.0' as const, + id, + error: { + code, + message, + }, + }; +} + +/** + * Creates a mock fetch function for HTTP transport testing + */ +export function createMockFetch(responses: Array<{ result?: unknown; error?: { code: number; message: string } }>) { + let callIndex = 0; + + return vi.fn(async (_url: string, _options: RequestInit) => { + const response = responses[callIndex] || responses[responses.length - 1]; + callIndex++; + + const body = response.error + ? createJsonRpcError(response.error.code, response.error.message, callIndex) + : createJsonRpcResponse(response.result, callIndex); + + return { + ok: true, + status: 200, + statusText: 'OK', + json: async () => body, + } as Response; + }); +} diff --git a/validator/tests/reporters/report-generator.test.ts b/validator/tests/reporters/report-generator.test.ts new file mode 100644 index 0000000..2d51a0d --- /dev/null +++ b/validator/tests/reporters/report-generator.test.ts @@ -0,0 +1,320 @@ +/** + * Report Generator Tests + * + * Tests the compliance report generation from validation results. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { ReportGenerator } from '../../src/reporters/index.js'; +import { + createServerInfo, + createToolValidationResult, + createPassingResult, + createFailingResult, +} from '../fixtures/index.js'; +import { ToolValidationResult, ValidationResult } from '../../src/types.js'; +import { ONX_TOOLS } from '@onx/schemas'; + +describe('ReportGenerator', () => { + let generator: ReportGenerator; + + beforeEach(() => { + generator = new ReportGenerator(); + }); + + describe('generate', () => { + it('should generate report with correct server info', () => { + const serverInfo = createServerInfo({ + name: 'my-server', + version: '2.0.0', + protocolVersion: '2024-11-05', + }); + const toolResults: ToolValidationResult[] = []; + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'http'); + + expect(report.serverInfo).toEqual(serverInfo); + expect(report.transport).toBe('http'); + }); + + it('should generate valid timestamp', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = []; + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.timestamp).toBeDefined(); + expect(new Date(report.timestamp).getTime()).not.toBeNaN(); + }); + + it('should calculate summary correctly with all tools passing', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = ONX_TOOLS.map(tool => + createToolValidationResult(tool, { exists: true, schemaValid: true }) + ); + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.summary.totalTools).toBe(ONX_TOOLS.length); + expect(report.summary.onXToolsImplemented).toBe(ONX_TOOLS.length); + expect(report.summary.missingTools).toBe(0); + }); + + it('should identify missing tools', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = ONX_TOOLS.slice(0, 6).map(tool => + createToolValidationResult(tool, { exists: true }) + ); + for (const tool of ONX_TOOLS.slice(6)) { + toolResults.push( + createToolValidationResult(tool, { + exists: false, + errors: [createFailingResult(tool, 'tool-exists', `Missing: ${tool}`)], + }) + ); + } + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.summary.missingTools).toBeGreaterThan(0); + expect(report.summary.onXToolsImplemented).toBeLessThan(ONX_TOOLS.length); + }); + + it('should merge functional results into tool results', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = [ + createToolValidationResult('get-orders', { exists: true }), + ]; + const functionalResults = new Map([ + [ + 'get-orders', + [ + createPassingResult('get-orders', 'functional-test-1'), + createFailingResult('get-orders', 'functional-test-2', 'Failed test'), + ], + ], + ]); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + const getOrdersResult = report.tools.find(t => t.tool === 'get-orders'); + expect(getOrdersResult?.functionalValid).toBe(false); + expect(getOrdersResult?.errors).toContainEqual( + expect.objectContaining({ check: 'functional-test-2' }) + ); + }); + + it('should set functionalValid to true when all functional tests pass', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = [ + createToolValidationResult('get-orders', { exists: true }), + ]; + const functionalResults = new Map([ + [ + 'get-orders', + [ + createPassingResult('get-orders', 'functional-test-1'), + createPassingResult('get-orders', 'functional-test-2'), + ], + ], + ]); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + const getOrdersResult = report.tools.find(t => t.tool === 'get-orders'); + expect(getOrdersResult?.functionalValid).toBe(true); + }); + + it('should count passed and failed checks correctly', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = [ + createToolValidationResult('get-orders', { + exists: true, + errors: [ + createPassingResult('get-orders', 'check-1'), + createPassingResult('get-orders', 'check-2'), + createFailingResult('get-orders', 'check-3'), + ], + }), + ]; + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.summary.passedChecks).toBe(3); + expect(report.summary.failedChecks).toBe(1); + }); + + it('should count warnings', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = [ + createToolValidationResult('get-orders', { + exists: true, + warnings: [ + createPassingResult('get-orders', 'warning-1'), + createPassingResult('get-orders', 'warning-2'), + ], + }), + ]; + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.summary.warnings).toBe(2); + }); + + describe('compliance level', () => { + it('should be "full" when all tools exist and all checks pass', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = ONX_TOOLS.map(tool => + createToolValidationResult(tool, { exists: true, errors: [] }) + ); + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.compliance).toBe('full'); + }); + + it('should be "non-compliant" when no tools are implemented', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = ONX_TOOLS.map((tool) => + createToolValidationResult(tool, { + exists: false, + errors: [createFailingResult(tool, 'tool-exists', `Missing: ${tool}`)], + }) + ); + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.compliance).toBe('non-compliant'); + }); + + it('should be "partial" when any tools are implemented', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = ONX_TOOLS.map((tool, index) => + createToolValidationResult(tool, { + exists: index < 2, + errors: index >= 2 + ? [createFailingResult(tool, 'tool-exists', `Missing: ${tool}`)] + : [], + }) + ); + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.compliance).toBe('partial'); + }); + + it('should be "partial" when all tools exist but some checks fail', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = ONX_TOOLS.map(tool => + createToolValidationResult(tool, { + exists: true, + errors: [createFailingResult(tool, 'schema-check', 'Schema error')], + }) + ); + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.compliance).toBe('partial'); + }); + }); + + describe('score calculation', () => { + it('should be 100 when all checks pass', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = ONX_TOOLS.map(tool => + createToolValidationResult(tool, { exists: true, errors: [] }) + ); + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.score).toBe(100); + }); + + it('should be 0 when no checks pass', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = ONX_TOOLS.map(tool => + createToolValidationResult(tool, { + exists: false, + errors: [createFailingResult(tool, 'tool-exists')], + }) + ); + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.score).toBe(0); + }); + + it('should calculate percentage correctly', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = [ + createToolValidationResult('get-orders', { + exists: true, + errors: [ + createPassingResult('get-orders', 'check-1'), + createFailingResult('get-orders', 'check-2'), + ], + }), + ]; + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + + expect(report.score).toBe(67); + }); + }); + }); + + describe('toJSON', () => { + it('should return valid JSON string', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = []; + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + const json = ReportGenerator.toJSON(report); + + expect(() => JSON.parse(json)).not.toThrow(); + }); + + it('should be properly formatted with indentation', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = []; + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'stdio'); + const json = ReportGenerator.toJSON(report); + + expect(json).toContain('\n'); + expect(json).toContain('"serverInfo"'); + }); + + it('should roundtrip correctly', () => { + const serverInfo = createServerInfo(); + const toolResults: ToolValidationResult[] = ONX_TOOLS.map(tool => + createToolValidationResult(tool, { exists: true }) + ); + const functionalResults = new Map(); + + const report = generator.generate(serverInfo, toolResults, functionalResults, 'http'); + const json = ReportGenerator.toJSON(report); + const parsed = JSON.parse(json); + + expect(parsed.serverInfo).toEqual(report.serverInfo); + expect(parsed.transport).toBe(report.transport); + expect(parsed.compliance).toBe(report.compliance); + expect(parsed.score).toBe(report.score); + }); + }); +}); diff --git a/validator/tests/setup.ts b/validator/tests/setup.ts new file mode 100644 index 0000000..a743604 --- /dev/null +++ b/validator/tests/setup.ts @@ -0,0 +1,20 @@ +/** + * Test Setup + * + * This file runs before each test file and provides: + * - Global test utilities + * - Mock factories + * - Common test fixtures + */ + +import { vi, beforeEach, afterAll } from 'vitest'; + +// Reset all mocks between tests +beforeEach(() => { + vi.clearAllMocks(); +}); + +// Clean up after all tests +afterAll(() => { + vi.restoreAllMocks(); +}); diff --git a/validator/tests/transports/http.test.ts b/validator/tests/transports/http.test.ts new file mode 100644 index 0000000..f606060 --- /dev/null +++ b/validator/tests/transports/http.test.ts @@ -0,0 +1,351 @@ +/** + * HTTP Transport Tests + * + * Tests the HTTP transport implementation for communicating with + * HTTP-based MCP servers. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { HttpTransport } from '../../src/transports/index.js'; +import { createMockFetch, createServerInfo } from '../fixtures/index.js'; + +// Mock the global fetch +const originalFetch = global.fetch; + +describe('HttpTransport', () => { + let mockFetch: ReturnType; + + beforeEach(() => { + mockFetch = vi.fn(); + global.fetch = mockFetch as unknown as typeof fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + describe('constructor', () => { + it('should create instance with URL config', () => { + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + expect(transport).toBeInstanceOf(HttpTransport); + expect(transport.isConnected()).toBe(false); + }); + + it('should create instance with headers', () => { + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + headers: { + Authorization: 'Bearer token123', + }, + }); + + expect(transport).toBeInstanceOf(HttpTransport); + }); + }); + + describe('connect', () => { + it('should send initialize request and store server info', async () => { + mockFetch = createMockFetch([ + { + result: { + serverInfo: { name: 'test-server', version: '1.0.0' }, + protocolVersion: '2024-11-05', + }, + }, + { result: {} }, // notifications/initialized + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await transport.connect(); + + expect(transport.isConnected()).toBe(true); + expect(mockFetch).toHaveBeenCalledTimes(2); + + // Verify initialize request + const initCall = mockFetch.mock.calls[0]; + expect(initCall[0]).toBe('http://localhost:3000/mcp'); + const initBody = JSON.parse(initCall[1].body as string); + expect(initBody.method).toBe('initialize'); + expect(initBody.params.clientInfo.name).toBe('onx-validator'); + }); + + it('should throw error on initialize failure', async () => { + mockFetch = createMockFetch([ + { error: { code: -32600, message: 'Invalid request' } }, + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await expect(transport.connect()).rejects.toThrow('Initialize failed'); + expect(transport.isConnected()).toBe(false); + }); + + it('should include custom headers in requests', async () => { + mockFetch = createMockFetch([ + { + result: { + serverInfo: { name: 'test-server', version: '1.0.0' }, + protocolVersion: '2024-11-05', + }, + }, + { result: {} }, + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + headers: { Authorization: 'Bearer secret' }, + }); + + await transport.connect(); + + const initCall = mockFetch.mock.calls[0]; + expect(initCall[1].headers).toMatchObject({ + 'Content-Type': 'application/json', + Authorization: 'Bearer secret', + }); + }); + }); + + describe('disconnect', () => { + it('should reset connection state', async () => { + mockFetch = createMockFetch([ + { + result: { + serverInfo: { name: 'test-server', version: '1.0.0' }, + protocolVersion: '2024-11-05', + }, + }, + { result: {} }, + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await transport.connect(); + expect(transport.isConnected()).toBe(true); + + await transport.disconnect(); + expect(transport.isConnected()).toBe(false); + }); + }); + + describe('getServerInfo', () => { + it('should return server info after connection', async () => { + mockFetch = createMockFetch([ + { + result: { + serverInfo: { name: 'my-server', version: '2.0.0' }, + protocolVersion: '2024-11-05', + }, + }, + { result: {} }, + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await transport.connect(); + const info = await transport.getServerInfo(); + + expect(info).toEqual({ + name: 'my-server', + version: '2.0.0', + protocolVersion: '2024-11-05', + }); + }); + + it('should throw error when not connected', async () => { + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await expect(transport.getServerInfo()).rejects.toThrow('Not connected'); + }); + }); + + describe('listTools', () => { + it('should return list of tools', async () => { + const tools = [ + { name: 'tool1', description: 'First tool', inputSchema: { type: 'object' } }, + { name: 'tool2', description: 'Second tool', inputSchema: { type: 'object' } }, + ]; + + mockFetch = createMockFetch([ + { + result: { + serverInfo: { name: 'server', version: '1.0.0' }, + protocolVersion: '2024-11-05', + }, + }, + { result: {} }, + { result: { tools } }, + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await transport.connect(); + const result = await transport.listTools(); + + expect(result).toEqual(tools); + + // Verify tools/list request + const listCall = mockFetch.mock.calls[2]; + const listBody = JSON.parse(listCall[1].body as string); + expect(listBody.method).toBe('tools/list'); + }); + + it('should throw error on list failure', async () => { + mockFetch = createMockFetch([ + { + result: { + serverInfo: { name: 'server', version: '1.0.0' }, + protocolVersion: '2024-11-05', + }, + }, + { result: {} }, + { error: { code: -32601, message: 'Method not found' } }, + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await transport.connect(); + await expect(transport.listTools()).rejects.toThrow('Failed to list tools'); + }); + }); + + describe('callTool', () => { + it('should call tool and return success response', async () => { + mockFetch = createMockFetch([ + { + result: { + serverInfo: { name: 'server', version: '1.0.0' }, + protocolVersion: '2024-11-05', + }, + }, + { result: {} }, + { + result: { + content: [{ type: 'text', text: 'Order created: ORD-123' }], + }, + }, + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await transport.connect(); + const response = await transport.callTool('create-sales-order', { + order: { lineItems: [{ sku: 'ABC', quantity: 1 }] }, + }); + + expect(response.isError).toBe(false); + expect(response.content).toEqual([{ type: 'text', text: 'Order created: ORD-123' }]); + }); + + it('should return error response on RPC error', async () => { + mockFetch = createMockFetch([ + { + result: { + serverInfo: { name: 'server', version: '1.0.0' }, + protocolVersion: '2024-11-05', + }, + }, + { result: {} }, + { error: { code: -32602, message: 'Invalid params' } }, + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await transport.connect(); + const response = await transport.callTool('create-sales-order', {}); + + expect(response.isError).toBe(true); + expect(response.content).toEqual([{ type: 'text', text: 'Invalid params' }]); + }); + + it('should handle isError in response', async () => { + mockFetch = createMockFetch([ + { + result: { + serverInfo: { name: 'server', version: '1.0.0' }, + protocolVersion: '2024-11-05', + }, + }, + { result: {} }, + { + result: { + content: [{ type: 'text', text: 'Order not found' }], + isError: true, + }, + }, + ]); + global.fetch = mockFetch as unknown as typeof fetch; + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await transport.connect(); + const response = await transport.callTool('get-orders', { ids: ['invalid'] }); + + expect(response.isError).toBe(true); + expect(response.content).toEqual([{ type: 'text', text: 'Order not found' }]); + }); + }); + + describe('HTTP error handling', () => { + it('should throw on HTTP error status', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + } as Response); + + const transport = new HttpTransport({ + type: 'http', + url: 'http://localhost:3000/mcp', + }); + + await expect(transport.connect()).rejects.toThrow('HTTP error: 500'); + }); + }); +}); diff --git a/validator/tests/validators/functional-validator.test.ts b/validator/tests/validators/functional-validator.test.ts new file mode 100644 index 0000000..8dac8ba --- /dev/null +++ b/validator/tests/validators/functional-validator.test.ts @@ -0,0 +1,193 @@ +/** + * Functional Validator Tests + * + * Tests the schema-driven functional validation that generates test cases + * from JSON schemas and calls tools to verify they work correctly. + */ + +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { FunctionalValidator } from '../../src/validators/index.js'; +import { createMockTransport } from '../fixtures/index.js'; +import type { McpTransport } from '../../src/transports/index.js'; +import { ONX_TOOLS } from '@onx/schemas'; + +describe('FunctionalValidator', () => { + let mockTransport: McpTransport; + let validator: FunctionalValidator; + + beforeEach(() => { + mockTransport = createMockTransport(); + validator = new FunctionalValidator(mockTransport); + }); + + describe('runAllTests', () => { + it('should run tests for all required tools', async () => { + await mockTransport.connect(); + const results = await validator.runAllTests(); + + expect(results.size).toBe(ONX_TOOLS.length); + for (const tool of ONX_TOOLS) { + expect(results.has(tool)).toBe(true); + } + }); + + it('should return validation results for each tool', async () => { + await mockTransport.connect(); + const results = await validator.runAllTests(); + + for (const [tool, toolResults] of results) { + expect(Array.isArray(toolResults)).toBe(true); + toolResults.forEach(result => { + expect(result).toHaveProperty('passed'); + expect(result).toHaveProperty('tool'); + expect(result).toHaveProperty('check'); + expect(result).toHaveProperty('message'); + expect(result.tool).toBe(tool); + }); + } + }); + }); + + describe('runToolTests', () => { + describe('schema-driven test generation', () => { + it('should generate empty-input test for tools with required fields', async () => { + await mockTransport.connect(); + // get-inventory has required field 'skus' + const results = await validator.runToolTests('get-inventory'); + + const emptyInputResult = results.find(r => r.check === 'functional-empty-input'); + expect(emptyInputResult).toBeDefined(); + }); + + it('should NOT generate empty-input test for tools with no required fields', async () => { + await mockTransport.connect(); + // get-orders has no required fields + const results = await validator.runToolTests('get-orders'); + + const emptyInputResult = results.find(r => r.check === 'functional-empty-input'); + expect(emptyInputResult).toBeUndefined(); + }); + + it('should pass empty-input when server returns isError: true', async () => { + mockTransport = createMockTransport({ + callToolResponse: (name, args) => { + // get-inventory requires 'skus' - server should reject empty input + if (name === 'get-inventory' && !args.skus) { + return { content: [{ type: 'text', text: 'skus is required' }], isError: true }; + } + return { content: [{ type: 'text', text: 'OK' }], isError: false }; + }, + }); + validator = new FunctionalValidator(mockTransport); + + await mockTransport.connect(); + const results = await validator.runToolTests('get-inventory'); + + // empty-input expects isError: true, and we got it + const emptyInputResult = results.find(r => r.check === 'functional-empty-input'); + expect(emptyInputResult?.passed).toBe(true); + }); + + it('should generate missing-required tests for each required field', async () => { + await mockTransport.connect(); + // get-inventory has required field: skus + const results = await validator.runToolTests('get-inventory'); + + const missingSkusResult = results.find(r => r.check === 'functional-missing-required-skus'); + expect(missingSkusResult).toBeDefined(); + }); + + it('should generate schema-valid-input test', async () => { + await mockTransport.connect(); + const results = await validator.runToolTests('get-inventory'); + + const schemaValidResult = results.find(r => r.check === 'functional-schema-valid-input'); + expect(schemaValidResult).toBeDefined(); + }); + + it('should pass schema-valid-input for any valid MCP response', async () => { + await mockTransport.connect(); + const results = await validator.runToolTests('get-inventory'); + + // schema-valid-input accepts any response (isError true or false) + const schemaValidResult = results.find(r => r.check === 'functional-schema-valid-input'); + expect(schemaValidResult?.passed).toBe(true); + }); + + it('should pass schema-valid-input even when server returns isError: true', async () => { + mockTransport = createMockTransport({ + callToolResponse: { content: [{ type: 'text', text: 'Not found' }], isError: true }, + }); + validator = new FunctionalValidator(mockTransport); + + await mockTransport.connect(); + const results = await validator.runToolTests('get-orders'); + + // schema-valid-input doesn't care about isError value + const schemaValidResult = results.find(r => r.check === 'functional-schema-valid-input'); + expect(schemaValidResult?.passed).toBe(true); + }); + }); + + describe('response validation', () => { + it('should fail when response missing content array', async () => { + mockTransport = createMockTransport({ + callToolResponse: { isError: false } as any, + }); + validator = new FunctionalValidator(mockTransport); + + await mockTransport.connect(); + const results = await validator.runToolTests('get-orders'); + + const schemaValidResult = results.find(r => r.check === 'functional-schema-valid-input'); + expect(schemaValidResult?.passed).toBe(false); + expect(schemaValidResult?.message).toContain('content'); + }); + + it('should fail when response missing isError boolean', async () => { + mockTransport = createMockTransport({ + callToolResponse: { content: [] } as any, + }); + validator = new FunctionalValidator(mockTransport); + + await mockTransport.connect(); + const results = await validator.runToolTests('get-orders'); + + const schemaValidResult = results.find(r => r.check === 'functional-schema-valid-input'); + expect(schemaValidResult?.passed).toBe(false); + expect(schemaValidResult?.message).toContain('isError'); + }); + }); + + describe('error handling', () => { + it('should handle exceptions from tool calls', async () => { + mockTransport = createMockTransport(); + vi.mocked(mockTransport.callTool).mockRejectedValue(new Error('Network error')); + validator = new FunctionalValidator(mockTransport); + + await mockTransport.connect(); + const results = await validator.runToolTests('get-orders'); + + const errorResults = results.filter(r => !r.passed); + expect(errorResults.length).toBeGreaterThan(0); + expect(errorResults[0].message).toContain('exception'); + }); + + it('should fail empty-input when expected isError but got success', async () => { + // Server incorrectly accepts invalid input + mockTransport = createMockTransport({ + callToolResponse: { content: [{ type: 'text', text: 'OK' }], isError: false }, + }); + validator = new FunctionalValidator(mockTransport); + + await mockTransport.connect(); + // create-sales-order requires 'order', so empty-input should fail + const results = await validator.runToolTests('create-sales-order'); + + // empty-input expects isError: true but server returned isError: false + const emptyInputResult = results.find(r => r.check === 'functional-empty-input'); + expect(emptyInputResult?.passed).toBe(false); + }); + }); + }); +}); diff --git a/validator/tests/validators/tool-validator.test.ts b/validator/tests/validators/tool-validator.test.ts new file mode 100644 index 0000000..aa986cd --- /dev/null +++ b/validator/tests/validators/tool-validator.test.ts @@ -0,0 +1,357 @@ +/** + * Tool Validator Tests + * + * Tests the tool presence and schema validation logic against canonical schemas. + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { ToolValidator } from '../../src/validators/index.js'; +import { createToolDefinition, createCompliantToolSet } from '../fixtures/index.js'; +import { ONX_TOOLS } from '@onx/schemas'; + +describe('ToolValidator', () => { + let validator: ToolValidator; + + beforeEach(() => { + validator = new ToolValidator(); + }); + + describe('validateToolPresence', () => { + it('should pass when all required tools are present', () => { + const tools = createCompliantToolSet(); + const results = validator.validateToolPresence(tools); + + const failedResults = results.filter(r => !r.passed); + expect(failedResults).toHaveLength(0); + expect(results).toHaveLength(ONX_TOOLS.length); + }); + + it('should fail when required tools are missing', () => { + const allTools = createCompliantToolSet(); + const tools = allTools.slice(0, Math.ceil(allTools.length / 2)); + const results = validator.validateToolPresence(tools); + + const failedResults = results.filter(r => !r.passed); + expect(failedResults.length).toBeGreaterThan(0); + + // Check that missing tools are reported (last tool will always be missing when we take first half) + const lastTool = ONX_TOOLS[ONX_TOOLS.length - 1]; + const missingToolNames = failedResults.map(r => r.tool); + expect(missingToolNames).toContain(lastTool); + }); + + it('should return correct check type for each result', () => { + const tools = createCompliantToolSet(); + const results = validator.validateToolPresence(tools); + + results.forEach(result => { + expect(result.check).toBe('tool-exists'); + }); + }); + }); + + describe('validateToolSchema', () => { + it('should pass for a tool with schema matching canonical', () => { + const tool = createToolDefinition('cancel-order', { + inputSchema: { + type: 'object', + properties: { + orderId: { type: 'string' }, + reason: { type: 'string' }, + notifyCustomer: { type: 'boolean' }, + notes: { type: 'string' }, + lineItems: { + type: 'array', + items: { + type: 'object', + properties: { + sku: { type: 'string', minLength: 1 }, + quantity: { type: 'number', minimum: 1 }, + id: { type: 'string' }, + }, + required: ['sku', 'quantity'], + }, + }, + }, + required: ['orderId'], + }, + }); + + const results = validator.validateToolSchema(tool); + const failures = results.filter(r => !r.passed); + + expect(failures).toHaveLength(0); + }); + + it('should fail when required property is missing from schema', () => { + const tool = createToolDefinition('cancel-order', { + inputSchema: { + type: 'object', + properties: { + reason: { type: 'string' }, + }, + required: ['reason'], + }, + }); + + const results = validator.validateToolSchema(tool); + const failures = results.filter(r => !r.passed); + + expect(failures.length).toBeGreaterThan(0); + expect(failures.some(f => f.message.includes('orderId'))).toBe(true); + }); + + it('should fail when required field is not in required array', () => { + const tool = createToolDefinition('cancel-order', { + inputSchema: { + type: 'object', + properties: { + orderId: { type: 'string' }, + }, + required: [], // orderId should be required + }, + }); + + const results = validator.validateToolSchema(tool); + const failures = results.filter(r => !r.passed); + + expect(failures.some(f => + f.check === 'schema-missing-required' && f.message.includes('orderId') + )).toBe(true); + }); + + it('should fail when constraint is missing (minimum)', () => { + const tool = createToolDefinition('cancel-order', { + inputSchema: { + type: 'object', + properties: { + orderId: { type: 'string' }, + lineItems: { + type: 'array', + items: { + type: 'object', + properties: { + sku: { type: 'string', minLength: 1 }, + quantity: { type: 'number' }, // Missing minimum: 1 + }, + required: ['sku', 'quantity'], + }, + }, + }, + required: ['orderId'], + }, + }); + + const results = validator.validateToolSchema(tool); + const failures = results.filter(r => !r.passed); + + expect(failures.some(f => f.check === 'schema-missing-minimum')).toBe(true); + }); + + it('should fail when constraint value differs (minLength)', () => { + const tool = createToolDefinition('cancel-order', { + inputSchema: { + type: 'object', + properties: { + orderId: { type: 'string' }, + lineItems: { + type: 'array', + items: { + type: 'object', + properties: { + sku: { type: 'string', minLength: 5 }, // Should be 1 + quantity: { type: 'number', minimum: 1 }, + }, + required: ['sku', 'quantity'], + }, + }, + }, + required: ['orderId'], + }, + }); + + const results = validator.validateToolSchema(tool); + const failures = results.filter(r => !r.passed); + + expect(failures.some(f => f.check === 'schema-minLength-mismatch')).toBe(true); + }); + + it('should fail when tool has no description', () => { + const tool = { + name: 'get-orders', + inputSchema: { type: 'object', properties: {} }, + }; + + const results = validator.validateToolSchema(tool as any); + const descFailure = results.find(r => r.check === 'schema-description'); + + expect(descFailure?.passed).toBe(false); + }); + + it('should pass for tools not in spec)', () => { + const tool = createToolDefinition('custom-tool', { + inputSchema: { + type: 'object', + properties: { + foo: { type: 'string' }, + }, + }, + }); + + const results = validator.validateToolSchema(tool); + const failures = results.filter(r => !r.passed); + + expect(failures).toHaveLength(0); + expect(results.some(r => r.check === 'schema-extra-tool')).toBe(true); + }); + + it('should fail when inputSchema is missing', () => { + const tool = { + name: 'cancel-order', + description: 'Cancel an order', + }; + + const results = validator.validateToolSchema(tool as any); + const schemaFailure = results.find(r => r.check === 'schema-exists'); + + expect(schemaFailure?.passed).toBe(false); + }); + + it('should fail when server is too strict (extra required field)', () => { + const tool = createToolDefinition('cancel-order', { + inputSchema: { + type: 'object', + properties: { + orderId: { type: 'string' }, + reason: { type: 'string' }, + }, + required: ['orderId', 'reason'], // reason is not required in canonical + }, + }); + + const results = validator.validateToolSchema(tool); + const failures = results.filter(r => !r.passed); + + expect(failures.some(f => + f.check === 'schema-extra-required' && f.message.includes('reason') + )).toBe(true); + }); + + it('should fail when server has extra property and canonical has additionalProperties: false', () => { + // cancel-order canonical has additionalProperties: false + const tool = createToolDefinition('cancel-order', { + inputSchema: { + type: 'object', + properties: { + orderId: { type: 'string' }, + reason: { type: 'string' }, + notifyCustomer: { type: 'boolean' }, + notes: { type: 'string' }, + lineItems: { type: 'array' }, + customField: { type: 'string' }, // Extra property not in canonical + }, + required: ['orderId'], + }, + }); + + const results = validator.validateToolSchema(tool); + const failures = results.filter(r => !r.passed); + + expect(failures.some(f => + f.check === 'schema-extra-property' && f.message.includes('customField') + )).toBe(true); + }); + + it('should pass when nested object allows additionalProperties', () => { + // create-sales-order.order.discounts[] items have additionalProperties: {} + const tool = createToolDefinition('create-sales-order', { + inputSchema: { + type: 'object', + properties: { + order: { + type: 'object', + properties: { + lineItems: { + type: 'array', + items: { + type: 'object', + properties: { + sku: { type: 'string', minLength: 1 }, + quantity: { type: 'number', minimum: 1 }, + }, + required: ['sku', 'quantity'], + }, + }, + discounts: { + type: 'array', + items: { + type: 'object', + properties: { + customDiscountField: { type: 'string' }, // Extra property - allowed + }, + additionalProperties: {}, + }, + }, + }, + required: ['lineItems'], + }, + }, + required: ['order'], + }, + }); + + const results = validator.validateToolSchema(tool); + const extraPropertyFailure = results.find(r => + r.check === 'schema-extra-property' && r.message.includes('customDiscountField') + ); + + expect(extraPropertyFailure).toBeUndefined(); + }); + + }); + + describe('validateAll', () => { + it('should return results for all required tools', () => { + const tools = createCompliantToolSet(); + const results = validator.validateAll(tools); + + expect(results.length).toBeGreaterThanOrEqual(ONX_TOOLS.length); + + // Each required tool should have a result + for (const requiredTool of ONX_TOOLS) { + const result = results.find(r => r.tool === requiredTool); + expect(result).toBeDefined(); + expect(result?.exists).toBe(true); + } + }); + + it('should mark missing tools as not existing', () => { + const allTools = createCompliantToolSet(); + const tools = allTools.slice(0, Math.ceil(allTools.length / 2)); + const results = validator.validateAll(tools); + + const missingResults = results.filter(r => !r.exists); + expect(missingResults.length).toBeGreaterThan(0); + + missingResults.forEach(result => { + expect(result.schemaValid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + }); + + it('should include extra tools with warnings', () => { + const tools = [ + ...createCompliantToolSet(), + createToolDefinition('my-custom-tool'), + ]; + + const results = validator.validateAll(tools); + const extraResult = results.find(r => r.tool === 'my-custom-tool'); + + expect(extraResult).toBeDefined(); + expect(extraResult?.exists).toBe(true); + expect(extraResult?.warnings.length).toBeGreaterThan(0); + expect(extraResult?.warnings[0].check).toBe('extra-tool'); + }); + }); +}); diff --git a/validator/tsconfig.json b/validator/tsconfig.json new file mode 100644 index 0000000..b37b6b9 --- /dev/null +++ b/validator/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/validator/vitest.config.ts b/validator/vitest.config.ts new file mode 100644 index 0000000..f31c388 --- /dev/null +++ b/validator/vitest.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // Test environment + environment: 'node', + + // Enable globals (describe, it, expect, etc.) + globals: true, + + // Include test files + include: ['tests/**/*.test.ts'], + + // Global test timeout + testTimeout: 10000, + + // Coverage configuration + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts'], + exclude: ['src/cli/**', 'src/**/index.ts'], + }, + + // Setup files run before each test file + setupFiles: ['./tests/setup.ts'], + }, +});