@anton.andrusenko/shopify-mcp-admin 2.2.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,12 @@
1
1
  import {
2
2
  getConfig,
3
3
  log,
4
- sanitizeLogMessage
5
- } from "./chunk-5QMYOO4B.js";
4
+ redactTokens,
5
+ sanitizeErrorMessage,
6
+ sanitizeLogMessage,
7
+ sanitizeParams,
8
+ truncateOutput
9
+ } from "./chunk-CZJ7LSEO.js";
6
10
  import {
7
11
  getAuthMode,
8
12
  getConfiguredRole,
@@ -1308,12 +1312,580 @@ var zodToJsonSchema = (schema, options) => {
1308
1312
  return combined;
1309
1313
  };
1310
1314
 
1315
+ // src/logging/correlation.ts
1316
+ import { randomUUID } from "crypto";
1317
+
1318
+ // src/types/context.ts
1319
+ import { AsyncLocalStorage } from "async_hooks";
1320
+ function createSingleTenantContext(client, shopDomain) {
1321
+ return {
1322
+ client,
1323
+ shopDomain
1324
+ // userId and apiKeyId are undefined in single-tenant mode
1325
+ };
1326
+ }
1327
+ function createMultiTenantContext(client, shopDomain, tenantId, apiKeyId, allowedShops, options) {
1328
+ return {
1329
+ client,
1330
+ shopDomain,
1331
+ userId: tenantId,
1332
+ // Map tenantId to userId for compatibility
1333
+ apiKeyId,
1334
+ tenantId,
1335
+ allowedShops,
1336
+ correlationId: options?.correlationId,
1337
+ oauthClientId: options?.oauthClientId,
1338
+ oauthClientName: options?.oauthClientName
1339
+ };
1340
+ }
1341
+ var requestContextStorage = new AsyncLocalStorage();
1342
+ var fallbackContextStorage = /* @__PURE__ */ new Map();
1343
+ function setFallbackContext(key, context) {
1344
+ fallbackContextStorage.set(key, context);
1345
+ }
1346
+ function clearFallbackContext(key) {
1347
+ fallbackContextStorage.delete(key);
1348
+ }
1349
+ var currentContextKey;
1350
+ function setCurrentContextKey(key) {
1351
+ currentContextKey = key;
1352
+ }
1353
+ function getRequestContext() {
1354
+ const asyncContext = requestContextStorage.getStore();
1355
+ if (asyncContext) {
1356
+ return asyncContext;
1357
+ }
1358
+ if (currentContextKey) {
1359
+ return fallbackContextStorage.get(currentContextKey);
1360
+ }
1361
+ return void 0;
1362
+ }
1363
+ function isMultiTenantContext(context) {
1364
+ return "tenantId" in context && typeof context.tenantId === "string";
1365
+ }
1366
+
1367
+ // src/logging/correlation.ts
1368
+ var CORRELATION_ID_HEADER = "X-Correlation-ID";
1369
+ var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1370
+ function generateCorrelationId() {
1371
+ return randomUUID();
1372
+ }
1373
+ function isValidCorrelationId(id) {
1374
+ return UUID_V4_REGEX.test(id);
1375
+ }
1376
+ function correlationMiddleware(req, res, next) {
1377
+ const headerValue = req.headers[CORRELATION_ID_HEADER.toLowerCase()];
1378
+ let correlationId;
1379
+ if (headerValue && isValidCorrelationId(headerValue)) {
1380
+ correlationId = headerValue;
1381
+ } else {
1382
+ correlationId = generateCorrelationId();
1383
+ }
1384
+ res.locals.correlationId = correlationId;
1385
+ if (typeof res.set === "function") {
1386
+ res.set(
1387
+ CORRELATION_ID_HEADER,
1388
+ correlationId
1389
+ );
1390
+ } else {
1391
+ res.setHeader(CORRELATION_ID_HEADER, correlationId);
1392
+ }
1393
+ next();
1394
+ }
1395
+
1396
+ // src/logging/structured-logger.ts
1397
+ import {
1398
+ pino as createPino,
1399
+ destination as pinoDestination,
1400
+ transport as pinoTransport
1401
+ } from "pino";
1402
+
1403
+ // src/monitoring/sentry.ts
1404
+ import * as Sentry from "@sentry/node";
1405
+ function initSentry(options) {
1406
+ const { dsn, environment = "production", release } = options;
1407
+ if (!dsn) {
1408
+ return;
1409
+ }
1410
+ Sentry.init({
1411
+ dsn,
1412
+ environment,
1413
+ // Sample 10% of transactions for performance monitoring (AC-13.7.4)
1414
+ tracesSampleRate: 0.1,
1415
+ // Release tracking for deployment correlation
1416
+ release: release || "unknown",
1417
+ // Sanitize sensitive data before sending to Sentry
1418
+ beforeSend(event) {
1419
+ if (event.request?.headers) {
1420
+ const sensitiveHeaders = [
1421
+ "authorization",
1422
+ "x-api-key",
1423
+ "cookie",
1424
+ "x-shopify-access-token",
1425
+ "x-shopify-hmac-sha256"
1426
+ ];
1427
+ for (const header of sensitiveHeaders) {
1428
+ delete event.request.headers[header];
1429
+ }
1430
+ }
1431
+ if (event.request?.data) {
1432
+ const data = event.request.data;
1433
+ const sensitiveKeys = ["token", "password", "secret", "key", "access_token"];
1434
+ for (const key of Object.keys(data)) {
1435
+ if (sensitiveKeys.some((sensitive) => key.toLowerCase().includes(sensitive))) {
1436
+ data[key] = "[REDACTED]";
1437
+ }
1438
+ }
1439
+ }
1440
+ if (event.message) {
1441
+ event.message = sanitizeErrorMessage2(event.message);
1442
+ }
1443
+ if (event.exception?.values) {
1444
+ for (const exception of event.exception.values) {
1445
+ if (exception.value) {
1446
+ exception.value = sanitizeErrorMessage2(exception.value);
1447
+ }
1448
+ }
1449
+ }
1450
+ return event;
1451
+ },
1452
+ // Integrations
1453
+ integrations: [
1454
+ // Capture uncaught exceptions and unhandled rejections
1455
+ Sentry.onUncaughtExceptionIntegration(),
1456
+ Sentry.onUnhandledRejectionIntegration(),
1457
+ // Capture HTTP request data
1458
+ Sentry.requestDataIntegration(),
1459
+ // Deduplicate similar errors
1460
+ Sentry.dedupeIntegration()
1461
+ ]
1462
+ });
1463
+ }
1464
+ function sanitizeErrorMessage2(message) {
1465
+ const patterns = [
1466
+ /shpat_[a-zA-Z0-9]+/g,
1467
+ /shpua_[a-zA-Z0-9]+/g,
1468
+ /Bearer\s+[a-zA-Z0-9_-]+/g,
1469
+ /access_token[=:]\s*[a-zA-Z0-9_-]+/gi,
1470
+ /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi,
1471
+ /sk_live_[a-zA-Z0-9_-]+/g
1472
+ ];
1473
+ let sanitized = message;
1474
+ for (const pattern of patterns) {
1475
+ sanitized = sanitized.replace(pattern, "[REDACTED]");
1476
+ }
1477
+ return sanitized;
1478
+ }
1479
+ function sentryRequestMiddleware(req, res, next) {
1480
+ const context = getRequestContext();
1481
+ if (context) {
1482
+ if (context.correlationId) {
1483
+ Sentry.setTag("correlationId", context.correlationId);
1484
+ Sentry.setContext("request", {
1485
+ correlationId: context.correlationId
1486
+ });
1487
+ }
1488
+ if (isMultiTenantContext(context) && context.tenantId) {
1489
+ Sentry.setTag("tenantId", context.tenantId);
1490
+ Sentry.setContext("tenant", {
1491
+ tenantId: context.tenantId
1492
+ });
1493
+ }
1494
+ if (context.shopDomain) {
1495
+ Sentry.setTag("shopDomain", context.shopDomain);
1496
+ }
1497
+ }
1498
+ Sentry.setContext("http", {
1499
+ method: req.method,
1500
+ url: req.url,
1501
+ path: req.path,
1502
+ query: req.query,
1503
+ userAgent: req.get("user-agent"),
1504
+ ip: req.ip
1505
+ });
1506
+ Sentry.addBreadcrumb({
1507
+ category: "http",
1508
+ message: `${req.method} ${req.path}`,
1509
+ level: "info",
1510
+ data: {
1511
+ method: req.method,
1512
+ path: req.path
1513
+ // statusCode will be updated when response finishes (see below)
1514
+ }
1515
+ });
1516
+ res.on("finish", () => {
1517
+ Sentry.addBreadcrumb({
1518
+ category: "http",
1519
+ message: `${req.method} ${req.path} - ${res.statusCode}`,
1520
+ level: res.statusCode >= 400 ? "error" : "info",
1521
+ data: {
1522
+ method: req.method,
1523
+ path: req.path,
1524
+ statusCode: res.statusCode
1525
+ }
1526
+ });
1527
+ });
1528
+ next();
1529
+ }
1530
+ function sentryErrorHandler(error, req, _res, next) {
1531
+ Sentry.captureException(error, {
1532
+ tags: {
1533
+ path: req.path,
1534
+ method: req.method
1535
+ },
1536
+ extra: {
1537
+ url: req.url,
1538
+ query: req.query,
1539
+ body: req.body
1540
+ }
1541
+ });
1542
+ next(error);
1543
+ }
1544
+ function captureError(error, context) {
1545
+ const requestContext = getRequestContext();
1546
+ Sentry.withScope((scope) => {
1547
+ if (requestContext?.correlationId) {
1548
+ scope.setTag("correlationId", requestContext.correlationId);
1549
+ scope.setContext("request", {
1550
+ correlationId: requestContext.correlationId
1551
+ });
1552
+ }
1553
+ if (requestContext && isMultiTenantContext(requestContext) && requestContext.tenantId) {
1554
+ scope.setTag("tenantId", requestContext.tenantId);
1555
+ scope.setContext("tenant", {
1556
+ tenantId: requestContext.tenantId
1557
+ });
1558
+ }
1559
+ if (requestContext?.shopDomain) {
1560
+ scope.setTag("shopDomain", requestContext.shopDomain);
1561
+ }
1562
+ if (context) {
1563
+ scope.setContext("additional", context);
1564
+ }
1565
+ Sentry.captureException(error);
1566
+ });
1567
+ }
1568
+
1569
+ // src/logging/structured-logger.ts
1570
+ function sanitizeText(text) {
1571
+ return redactTokens(text);
1572
+ }
1573
+ function sanitizeObject(obj, seen = /* @__PURE__ */ new WeakSet()) {
1574
+ if (typeof obj === "string") {
1575
+ return sanitizeText(obj);
1576
+ }
1577
+ if (obj === null || typeof obj !== "object") {
1578
+ return obj;
1579
+ }
1580
+ if (seen.has(obj)) {
1581
+ return "[Circular]";
1582
+ }
1583
+ seen.add(obj);
1584
+ if (Array.isArray(obj)) {
1585
+ return obj.map((item) => sanitizeObject(item, seen));
1586
+ }
1587
+ const result = {};
1588
+ for (const [key, value] of Object.entries(obj)) {
1589
+ result[key] = sanitizeObject(value, seen);
1590
+ }
1591
+ return result;
1592
+ }
1593
+ function getInstanceId() {
1594
+ return process.env.INSTANCE_ID || process.env.HOSTNAME || "unknown";
1595
+ }
1596
+ function getLogLevel() {
1597
+ const level = process.env.LOG_LEVEL?.toLowerCase();
1598
+ if (level === "debug" || level === "info" || level === "warn" || level === "error") {
1599
+ return level;
1600
+ }
1601
+ return "info";
1602
+ }
1603
+ function isDevelopment() {
1604
+ return process.env.NODE_ENV === "development";
1605
+ }
1606
+ function createTransport() {
1607
+ if (isDevelopment()) {
1608
+ try {
1609
+ return {
1610
+ target: "pino-pretty",
1611
+ options: {
1612
+ destination: 2,
1613
+ // stderr (fd 2)
1614
+ colorize: true,
1615
+ translateTime: "HH:MM:ss.l",
1616
+ ignore: "pid,hostname"
1617
+ }
1618
+ };
1619
+ } catch {
1620
+ return void 0;
1621
+ }
1622
+ }
1623
+ return void 0;
1624
+ }
1625
+ function createBasePinoLogger() {
1626
+ const level = getLogLevel();
1627
+ const transport = createTransport();
1628
+ const instanceId = getInstanceId();
1629
+ const options = {
1630
+ level,
1631
+ // ISO 8601 timestamp format (AC-13.1.3)
1632
+ timestamp: () => `,"time":"${(/* @__PURE__ */ new Date()).toISOString()}"`,
1633
+ // Format level as string instead of number
1634
+ formatters: {
1635
+ level: (label) => ({ level: label })
1636
+ },
1637
+ // Base bindings (can be overridden by child loggers)
1638
+ // Story 13.5: Add instance ID for multi-instance deployment identification
1639
+ base: {
1640
+ instanceId
1641
+ // AC-13.5.1: Instance ID for load balancer distribution verification
1642
+ }
1643
+ };
1644
+ if (transport) {
1645
+ return createPino(options, pinoTransport(transport));
1646
+ }
1647
+ return createPino(options, pinoDestination({ dest: 2, sync: false }));
1648
+ }
1649
+ var basePinoLogger = createBasePinoLogger();
1650
+ function createLogger(module) {
1651
+ function getContextFields() {
1652
+ const context = getRequestContext();
1653
+ const fields = { module };
1654
+ if (context) {
1655
+ if (context.correlationId) {
1656
+ fields.correlationId = context.correlationId;
1657
+ }
1658
+ if (context.shopDomain) {
1659
+ fields.shopDomain = context.shopDomain;
1660
+ }
1661
+ if (isMultiTenantContext(context)) {
1662
+ fields.tenantId = context.tenantId;
1663
+ }
1664
+ }
1665
+ return fields;
1666
+ }
1667
+ function mergeData(data) {
1668
+ const contextFields = getContextFields();
1669
+ if (data) {
1670
+ const sanitizedData = sanitizeObject(data);
1671
+ return { ...contextFields, ...sanitizedData };
1672
+ }
1673
+ return contextFields;
1674
+ }
1675
+ function formatError(error) {
1676
+ return {
1677
+ error: {
1678
+ name: error.name,
1679
+ message: sanitizeText(error.message),
1680
+ stack: error.stack ? sanitizeText(error.stack) : void 0
1681
+ }
1682
+ };
1683
+ }
1684
+ const logger2 = {
1685
+ debug: (msg, data) => {
1686
+ basePinoLogger.debug(mergeData(data), sanitizeText(msg));
1687
+ },
1688
+ info: (msg, data) => {
1689
+ basePinoLogger.info(mergeData(data), sanitizeText(msg));
1690
+ },
1691
+ warn: (msg, data) => {
1692
+ basePinoLogger.warn(mergeData(data), sanitizeText(msg));
1693
+ },
1694
+ error: (msg, error, data) => {
1695
+ const merged = mergeData(data);
1696
+ if (error) {
1697
+ Object.assign(merged, formatError(error));
1698
+ captureError(error, data);
1699
+ }
1700
+ basePinoLogger.error(merged, sanitizeText(msg));
1701
+ },
1702
+ child: (bindings) => {
1703
+ const childModule = bindings.module || module;
1704
+ const childLogger = createLogger(childModule);
1705
+ const originalMergeData = mergeData;
1706
+ const enhancedMergeData = (data) => {
1707
+ const base = originalMergeData(data);
1708
+ const sanitizedBindings = sanitizeObject(bindings);
1709
+ return { ...base, ...sanitizedBindings };
1710
+ };
1711
+ return {
1712
+ debug: (msg, data) => {
1713
+ basePinoLogger.debug(enhancedMergeData(data), sanitizeText(msg));
1714
+ },
1715
+ info: (msg, data) => {
1716
+ basePinoLogger.info(enhancedMergeData(data), sanitizeText(msg));
1717
+ },
1718
+ warn: (msg, data) => {
1719
+ basePinoLogger.warn(enhancedMergeData(data), sanitizeText(msg));
1720
+ },
1721
+ error: (msg, error, data) => {
1722
+ const merged = enhancedMergeData(data);
1723
+ if (error) {
1724
+ Object.assign(merged, formatError(error));
1725
+ }
1726
+ basePinoLogger.error(merged, sanitizeText(msg));
1727
+ },
1728
+ child: childLogger.child
1729
+ };
1730
+ }
1731
+ };
1732
+ return logger2;
1733
+ }
1734
+ function flushLogs() {
1735
+ basePinoLogger.flush();
1736
+ }
1737
+
1738
+ // src/logging/tool-execution-logger.ts
1739
+ import { Prisma } from "@prisma/client";
1740
+ var logger = createLogger("logging/tool-execution-logger");
1741
+ var VALIDATION_ERROR_KEYWORDS = [
1742
+ "validation failed",
1743
+ "required",
1744
+ "must be",
1745
+ "expected",
1746
+ "cannot be empty",
1747
+ "is not allowed",
1748
+ "malformed",
1749
+ "parse error",
1750
+ "zod"
1751
+ ];
1752
+ var AUTH_ERROR_KEYWORDS = [
1753
+ "unauthorized",
1754
+ "unauthenticated",
1755
+ "forbidden",
1756
+ "access denied",
1757
+ "not authorized",
1758
+ "authentication required",
1759
+ "authentication failed",
1760
+ "permission denied",
1761
+ "invalid api key",
1762
+ "invalid token",
1763
+ "token expired"
1764
+ ];
1765
+ var TIMEOUT_KEYWORDS = ["timeout", "timed out", "deadline exceeded", "econnreset", "socket hang"];
1766
+ function classifyError(error) {
1767
+ let message;
1768
+ if (error instanceof Error) {
1769
+ message = error.message.toLowerCase();
1770
+ } else if (error !== null && typeof error === "object" && "message" in error && typeof error.message === "string") {
1771
+ message = error.message.toLowerCase();
1772
+ } else {
1773
+ message = String(error).toLowerCase();
1774
+ }
1775
+ const errorName = error instanceof Error ? error.name : typeof error === "object" && error !== null ? "Object" : "Error";
1776
+ const isToolError = error !== null && typeof error === "object" && "name" in error && error.name === "ToolError";
1777
+ if (AUTH_ERROR_KEYWORDS.some((kw) => message.includes(kw))) {
1778
+ return {
1779
+ status: "AUTH_ERROR",
1780
+ errorCode: isToolError ? "TOOL_AUTH_ERROR" : "AUTH_ERROR"
1781
+ };
1782
+ }
1783
+ if (VALIDATION_ERROR_KEYWORDS.some((kw) => message.includes(kw))) {
1784
+ return {
1785
+ status: "VALIDATION_ERROR",
1786
+ errorCode: isToolError ? "TOOL_VALIDATION_ERROR" : "VALIDATION_ERROR"
1787
+ };
1788
+ }
1789
+ if (isToolError) {
1790
+ return { status: "ERROR", errorCode: "TOOL_ERROR" };
1791
+ }
1792
+ if (TIMEOUT_KEYWORDS.some((kw) => message.includes(kw))) {
1793
+ return { status: "TIMEOUT", errorCode: "TIMEOUT" };
1794
+ }
1795
+ if (message.includes("graphql") || errorName === "GraphqlQueryError") {
1796
+ return { status: "ERROR", errorCode: "GRAPHQL_ERROR" };
1797
+ }
1798
+ return { status: "ERROR", errorCode: "UNKNOWN_ERROR" };
1799
+ }
1800
+ var ToolExecutionLogger = class {
1801
+ prisma;
1802
+ /**
1803
+ * Create a new ToolExecutionLogger
1804
+ *
1805
+ * @param prisma - Prisma client for database access
1806
+ */
1807
+ constructor(prisma) {
1808
+ this.prisma = prisma;
1809
+ }
1810
+ /**
1811
+ * Log a tool execution (fire-and-forget)
1812
+ *
1813
+ * This method does NOT await the database insert. The insert runs
1814
+ * asynchronously and any errors are caught and logged, not thrown.
1815
+ *
1816
+ * AC-16.2.7: Fire-and-forget pattern with max 5ms overhead.
1817
+ *
1818
+ * @param entry - Tool execution entry to log
1819
+ *
1820
+ * @example
1821
+ * ```typescript
1822
+ * logger.logExecution({
1823
+ * tenantId: 'tenant-uuid',
1824
+ * toolName: 'create-product',
1825
+ * toolModule: 'products',
1826
+ * clientType: 'api_key',
1827
+ * apiKeyId: 'apikey-uuid',
1828
+ * inputParams: { title: 'New Product' },
1829
+ * output: { productId: '123' },
1830
+ * status: 'SUCCESS',
1831
+ * durationMs: 150,
1832
+ * });
1833
+ * // Returns immediately, database insert happens async
1834
+ * ```
1835
+ */
1836
+ logExecution(entry) {
1837
+ if (!entry.tenantId) {
1838
+ logger.debug("Skipping tool execution log: no tenantId", { toolName: entry.toolName });
1839
+ return;
1840
+ }
1841
+ const sanitizedInput = entry.inputParams ? sanitizeParams(entry.inputParams) : void 0;
1842
+ const sanitizedOutput = entry.output ? truncateOutput(entry.output) : void 0;
1843
+ const sanitizedErrorMessage = entry.errorMessage ? sanitizeErrorMessage(entry.errorMessage) : void 0;
1844
+ const logData = {
1845
+ tenant: { connect: { id: entry.tenantId } },
1846
+ toolName: entry.toolName,
1847
+ toolModule: entry.toolModule,
1848
+ sessionId: entry.sessionId,
1849
+ correlationId: entry.correlationId,
1850
+ clientType: entry.clientType,
1851
+ apiKey: entry.apiKeyId ? { connect: { id: entry.apiKeyId } } : void 0,
1852
+ oauthClientId: entry.oauthClientId,
1853
+ shop: entry.shopId ? { connect: { id: entry.shopId } } : void 0,
1854
+ shopDomain: entry.shopDomain,
1855
+ inputParams: sanitizedInput ?? Prisma.JsonNull,
1856
+ outputSummary: sanitizedOutput ?? Prisma.JsonNull,
1857
+ status: entry.status,
1858
+ errorCode: entry.errorCode,
1859
+ errorMessage: sanitizedErrorMessage,
1860
+ durationMs: entry.durationMs,
1861
+ ipAddress: entry.ipAddress,
1862
+ userAgent: entry.userAgent
1863
+ };
1864
+ this.prisma.toolExecutionLog.create({ data: logData }).catch((err) => {
1865
+ logger.error("Failed to log tool execution", err, {
1866
+ toolName: entry.toolName,
1867
+ tenantId: entry.tenantId?.substring(0, 8)
1868
+ });
1869
+ });
1870
+ }
1871
+ };
1872
+ var toolExecutionLoggerInstance = null;
1873
+ function getToolExecutionLogger(prisma) {
1874
+ if (!toolExecutionLoggerInstance) {
1875
+ if (!prisma) {
1876
+ throw new Error("ToolExecutionLogger not initialized. Call with Prisma client first.");
1877
+ }
1878
+ toolExecutionLoggerInstance = new ToolExecutionLogger(prisma);
1879
+ }
1880
+ return toolExecutionLoggerInstance;
1881
+ }
1882
+
1311
1883
  // src/shopify/client.ts
1312
1884
  import "@shopify/shopify-api/adapters/node";
1313
1885
  import { shopifyApi } from "@shopify/shopify-api";
1314
1886
 
1315
1887
  // src/utils/errors.ts
1316
- var sanitizeErrorMessage = sanitizeLogMessage;
1888
+ var sanitizeErrorMessage3 = sanitizeLogMessage;
1317
1889
  var ToolError = class _ToolError extends Error {
1318
1890
  /** AI-friendly suggestion for error recovery */
1319
1891
  suggestion;
@@ -1343,8 +1915,8 @@ function extractErrorMessage(error) {
1343
1915
  }
1344
1916
  function createToolError(error, suggestion) {
1345
1917
  const message = extractErrorMessage(error);
1346
- const safeMessage = sanitizeErrorMessage(message);
1347
- const safeSuggestion = sanitizeErrorMessage(suggestion);
1918
+ const safeMessage = sanitizeErrorMessage3(message);
1919
+ const safeSuggestion = sanitizeErrorMessage3(suggestion);
1348
1920
  return {
1349
1921
  isError: true,
1350
1922
  content: [
@@ -1385,8 +1957,7 @@ function createToolSuccess(data) {
1385
1957
  type: "text",
1386
1958
  text
1387
1959
  }
1388
- ],
1389
- structuredContent: data
1960
+ ]
1390
1961
  };
1391
1962
  }
1392
1963
 
@@ -1787,52 +2358,6 @@ async function validateShopifyToken(shopDomain, accessToken) {
1787
2358
  }
1788
2359
  }
1789
2360
 
1790
- // src/types/context.ts
1791
- import { AsyncLocalStorage } from "async_hooks";
1792
- function createSingleTenantContext(client, shopDomain) {
1793
- return {
1794
- client,
1795
- shopDomain
1796
- // userId and apiKeyId are undefined in single-tenant mode
1797
- };
1798
- }
1799
- function createMultiTenantContext(client, shopDomain, tenantId, apiKeyId, allowedShops) {
1800
- return {
1801
- client,
1802
- shopDomain,
1803
- userId: tenantId,
1804
- // Map tenantId to userId for compatibility
1805
- apiKeyId,
1806
- tenantId,
1807
- allowedShops
1808
- };
1809
- }
1810
- var requestContextStorage = new AsyncLocalStorage();
1811
- var fallbackContextStorage = /* @__PURE__ */ new Map();
1812
- function setFallbackContext(key, context) {
1813
- fallbackContextStorage.set(key, context);
1814
- }
1815
- function clearFallbackContext(key) {
1816
- fallbackContextStorage.delete(key);
1817
- }
1818
- var currentContextKey;
1819
- function setCurrentContextKey(key) {
1820
- currentContextKey = key;
1821
- }
1822
- function getRequestContext() {
1823
- const asyncContext = requestContextStorage.getStore();
1824
- if (asyncContext) {
1825
- return asyncContext;
1826
- }
1827
- if (currentContextKey) {
1828
- return fallbackContextStorage.get(currentContextKey);
1829
- }
1830
- return void 0;
1831
- }
1832
- function isMultiTenantContext(context) {
1833
- return "tenantId" in context && typeof context.tenantId === "string";
1834
- }
1835
-
1836
2361
  // src/tools/registration.ts
1837
2362
  var KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
1838
2363
  function deriveDefaultAnnotations(name) {
@@ -1888,18 +2413,73 @@ function validateInput(schema, input, toolName) {
1888
2413
  }
1889
2414
  return result.data;
1890
2415
  }
2416
+ function logToolExecution(toolName, params, result, status, startTime, error) {
2417
+ let config;
2418
+ try {
2419
+ config = getConfig();
2420
+ } catch {
2421
+ return;
2422
+ }
2423
+ if (!isRemoteMode(config)) {
2424
+ log.debug(`Tool execution logging skipped in local mode: ${toolName}`);
2425
+ return;
2426
+ }
2427
+ const context = getRequestContext();
2428
+ if (!context || !isMultiTenantContext(context)) {
2429
+ log.debug(`Tool execution logging skipped: no multi-tenant context for ${toolName}`);
2430
+ return;
2431
+ }
2432
+ const endTime = performance.now();
2433
+ const durationMs = Math.round(endTime - startTime);
2434
+ const multiTenantContext = context;
2435
+ const hasOAuthClient = !!multiTenantContext.oauthClientId;
2436
+ const clientType = hasOAuthClient ? "oauth_client" : "api_key";
2437
+ let errorClassification;
2438
+ if (typeof status === "object") {
2439
+ errorClassification = status;
2440
+ } else if (error) {
2441
+ errorClassification = classifyError(error);
2442
+ }
2443
+ const entry = {
2444
+ tenantId: multiTenantContext.tenantId,
2445
+ toolName,
2446
+ correlationId: context.correlationId,
2447
+ clientType,
2448
+ apiKeyId: !hasOAuthClient ? multiTenantContext.apiKeyId : void 0,
2449
+ oauthClientId: hasOAuthClient ? multiTenantContext.oauthClientId : void 0,
2450
+ shopDomain: context.shopDomain,
2451
+ inputParams: params,
2452
+ output: result,
2453
+ status: errorClassification ? errorClassification.status : "SUCCESS",
2454
+ errorCode: errorClassification?.errorCode,
2455
+ errorMessage: error instanceof Error ? error.message : error ? String(error) : void 0,
2456
+ durationMs
2457
+ };
2458
+ try {
2459
+ const logger2 = getToolExecutionLogger();
2460
+ logger2.logExecution(entry);
2461
+ } catch (logError) {
2462
+ log.debug(
2463
+ `Tool execution logger not available: ${logError instanceof Error ? logError.message : "unknown"}`
2464
+ );
2465
+ }
2466
+ }
1891
2467
  function wrapToolHandler(toolName, schema, handler) {
1892
2468
  return async (params) => {
2469
+ const startTime = performance.now();
1893
2470
  log.debug(`Tool invoked: ${toolName}`);
1894
2471
  try {
1895
2472
  const validatedParams = validateInput(schema, params, toolName);
1896
2473
  const result = await handler(validatedParams);
1897
2474
  log.debug(`Tool ${toolName} completed successfully`);
2475
+ logToolExecution(toolName, params, result, "SUCCESS", startTime);
1898
2476
  return createToolSuccess(result);
1899
2477
  } catch (error) {
1900
2478
  log.error(
1901
2479
  `Tool ${toolName} failed: ${error instanceof Error ? error.message : "Unknown error"}`
1902
2480
  );
2481
+ const errorClass = classifyError(error);
2482
+ logToolExecution(toolName, params, null, errorClass, startTime, error);
1903
2483
  let suggestion;
1904
2484
  if (error instanceof ToolError) {
1905
2485
  suggestion = error.suggestion;
@@ -2052,10 +2632,10 @@ function createHandlerWithContext(contextAwareHandler) {
2052
2632
  }
2053
2633
  if (isRemoteMode(config)) {
2054
2634
  log.error(
2055
- `[tool] Remote mode but no valid async context available. Context: ${asyncContext ? JSON.stringify(Object.keys(asyncContext)) : "undefined"}. This may indicate AsyncLocalStorage context was lost through MCP SDK async chain.`
2635
+ `[tool] Remote mode but no valid async context available. Context: ${asyncContext ? JSON.stringify(Object.keys(asyncContext)) : "undefined"}. This likely means no shop is connected or shop tokens need re-authentication.`
2056
2636
  );
2057
2637
  throw new Error(
2058
- "Request context not available. Please ensure you are authenticated and try again."
2638
+ "No connected Shopify store found. Please connect a store through the dashboard, or reconnect if your store was previously connected."
2059
2639
  );
2060
2640
  }
2061
2641
  const client = await getShopifyClient();
@@ -13981,7 +14561,11 @@ export {
13981
14561
  setCurrentContextKey,
13982
14562
  getRequestContext,
13983
14563
  isMultiTenantContext,
13984
- sanitizeErrorMessage,
14564
+ initSentry,
14565
+ sentryRequestMiddleware,
14566
+ sentryErrorHandler,
14567
+ captureError,
14568
+ sanitizeErrorMessage3 as sanitizeErrorMessage,
13985
14569
  createShopifyClient,
13986
14570
  getShopifyClient,
13987
14571
  validateShopifyToken,
@@ -13994,6 +14578,11 @@ export {
13994
14578
  getStorePolicies,
13995
14579
  getStoreAlerts,
13996
14580
  getStoreInfo,
14581
+ generateCorrelationId,
14582
+ correlationMiddleware,
14583
+ createLogger,
14584
+ flushLogs,
14585
+ getToolExecutionLogger,
13997
14586
  deriveDefaultAnnotations,
13998
14587
  validateToolName,
13999
14588
  isValidToolName,