diff --git a/magento/app.json b/magento/app.json index 0588894a..41cd3ff3 100644 --- a/magento/app.json +++ b/magento/app.json @@ -13,12 +13,6 @@ "title": "Base URL", "description": "Magento store base URL, e.g. https://loja.granado.com.br (no trailing slash, no /rest)" }, - "apiToken": { - "type": "string", - "title": "API Token", - "description": "Magento integration access token (sent as Authorization: Bearer)", - "format": "password" - }, "storeCode": { "type": "string", "title": "Store Code", @@ -49,7 +43,7 @@ } } }, - "required": ["baseUrl", "apiToken"] + "required": ["baseUrl"] } }, "description": "MCP for Magento 2 REST APIs — live sales dashboard widgets (orders per hour, sales cards, cancellation rate, top products, status breakdown) plus curated tools for orders, catalog, customers, inventory and CMS.", diff --git a/magento/server/lib/cache.ts b/magento/server/lib/cache.ts new file mode 100644 index 00000000..980ea625 --- /dev/null +++ b/magento/server/lib/cache.ts @@ -0,0 +1,24 @@ +const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes + +interface CacheEntry { + data: T; + expiresAt: number; +} + +const store = new Map>(); + +export async function getOrFetch( + key: string, + fetcher: () => Promise, + ttlMs = CACHE_TTL_MS, +): Promise { + const entry = store.get(key) as CacheEntry | undefined; + if (entry && entry.expiresAt > Date.now()) { + if (process.env.DEBUG) console.log("[Magento cache] HIT", key); + return entry.data; + } + if (process.env.DEBUG) console.log("[Magento cache] MISS", key); + const data = await fetcher(); + store.set(key, { data, expiresAt: Date.now() + ttlMs }); + return data; +} diff --git a/magento/server/lib/client.ts b/magento/server/lib/client.ts index cbb9014f..8116491b 100644 --- a/magento/server/lib/client.ts +++ b/magento/server/lib/client.ts @@ -4,6 +4,7 @@ * the withRetry logic from vtex/server/lib/tool-adapter.ts. */ import type { MagentoCredentials } from "../types/env.ts"; +import { getOrFetch } from "./cache.ts"; export type { MagentoCredentials }; @@ -14,7 +15,7 @@ const INITIAL_RETRY_DELAY_MS = 500; export const DEFAULT_STORE_CODE = "all"; export const DEFAULT_CURRENCY = "BRL"; -type CredentialSource = "state" | "env" | "missing"; +type CredentialSource = "authorization" | "state" | "env" | "missing"; export interface ResolvedCredentials extends MagentoCredentials { /** Where each credential value came from — useful for diagnosing missing config. */ @@ -35,15 +36,32 @@ function pickSource( return { value: "", source: "missing" }; } +// Structural subset of RequestContext from @decocms/runtime — token is the +// raw bearer value (no "Bearer " prefix) exposed by the runtime. +export interface MeshRequestContext { + token?: string; + state?: { + baseUrl?: string; + storeCode?: string; + currencyCode?: string; + timezone?: string; + originHeader?: string; + extraHeaders?: Record; + }; +} + export function resolveCredentials( - state: Partial | undefined, + ctx: MeshRequestContext | undefined, ): ResolvedCredentials { - const safeState = state ?? {}; + const safeState = ctx?.state ?? {}; + const baseUrl = pickSource(safeState.baseUrl, process.env.MAGENTO_BASE_URL); - const apiToken = pickSource( - safeState.apiToken, - process.env.MAGENTO_API_TOKEN, - ); + + // ctx.token is already the raw bearer value (no "Bearer " prefix) + const apiToken: { value: string; source: CredentialSource } = ctx?.token + ? { value: ctx.token, source: "authorization" } + : pickSource(undefined, process.env.MAGENTO_API_TOKEN); + const storeCode = pickSource( safeState.storeCode, process.env.MAGENTO_STORE_CODE, @@ -54,11 +72,10 @@ export function resolveCredentials( ); if (baseUrl.source === "missing") { - const stateKeys = state ? Object.keys(state) : []; + const stateKeys = safeState ? Object.keys(safeState) : []; console.warn( "[Magento] resolveCredentials: baseUrl is missing — neither MESH_REQUEST_CONTEXT.state.baseUrl nor process.env.MAGENTO_BASE_URL is set.", JSON.stringify({ - receivedStateType: state === undefined ? "undefined" : typeof state, receivedStateKeys: stateKeys, envMagentoBaseUrlSet: Boolean(process.env.MAGENTO_BASE_URL), }), @@ -94,7 +111,7 @@ export function assertValidCredentials( } if (!creds.apiToken) { throw new Error( - `Magento apiToken is missing${where} — set MESH_REQUEST_CONTEXT.state.apiToken or the MAGENTO_API_TOKEN env var (integration access token).`, + `Magento apiToken is missing${where} — set the Token field in the MCP connection (Authorization: Bearer) or the MAGENTO_API_TOKEN env var (integration access token).`, ); } } @@ -143,19 +160,15 @@ export interface MagentoFetchOptions { * some stores (e.g. Granado) sit behind a Cloudflare rule that requires the * x-origin-header secret. */ -export async function magentoFetch( +async function doFetch( creds: MagentoCredentials, + url: string, path: string, - options: MagentoFetchOptions = {}, + options: MagentoFetchOptions, ): Promise { - const url = buildRestUrl(creds, path, options.params); const headers = buildMagentoHeaders(creds); const method = options.method ?? "GET"; - if (process.env.DEBUG) { - console.log("[Magento]", method, url); - } - let lastError: Error | null = null; for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { let response: Response; @@ -205,3 +218,23 @@ export async function magentoFetch( throw lastError ?? new Error("Magento request failed"); } + +export async function magentoFetch( + creds: MagentoCredentials, + path: string, + options: MagentoFetchOptions = {}, +): Promise { + const method = options.method ?? "GET"; + const url = buildRestUrl(creds, path, options.params); + + if (process.env.DEBUG) { + console.log("[Magento]", method, url); + } + + if (method !== "GET") { + return doFetch(creds, url, path, options); + } + + const cacheKey = `${creds.baseUrl}|${creds.storeCode ?? "all"}|${url}`; + return getOrFetch(cacheKey, () => doFetch(creds, url, path, options)); +} diff --git a/magento/server/tools/custom/cancellation-rate.ts b/magento/server/tools/custom/cancellation-rate.ts index 5d8393c9..e11c6dc5 100644 --- a/magento/server/tools/custom/cancellation-rate.ts +++ b/magento/server/tools/custom/cancellation-rate.ts @@ -42,7 +42,7 @@ export const cancellationRate = (_env: Env) => annotations: { readOnlyHint: true }, execute: async ({ context, runtimeContext }) => { const env = runtimeContext.env as Env; - const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT?.state); + const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT); assertValidCredentials(creds, TOOL_ID); const timezone = creds.timezone ?? DEFAULT_STORE_TIMEZONE; diff --git a/magento/server/tools/custom/orders-sales-card.ts b/magento/server/tools/custom/orders-sales-card.ts index 3137a5eb..dc4fcf95 100644 --- a/magento/server/tools/custom/orders-sales-card.ts +++ b/magento/server/tools/custom/orders-sales-card.ts @@ -55,7 +55,7 @@ export const ordersSalesCard = (_env: Env) => annotations: { readOnlyHint: true }, execute: async ({ context, runtimeContext }) => { const env = runtimeContext.env as Env; - const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT?.state); + const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT); assertValidCredentials(creds, TOOL_ID); const timezone = creds.timezone ?? DEFAULT_STORE_TIMEZONE; diff --git a/magento/server/tools/custom/orders-timeline.ts b/magento/server/tools/custom/orders-timeline.ts index ebae4d8b..2b60a6a6 100644 --- a/magento/server/tools/custom/orders-timeline.ts +++ b/magento/server/tools/custom/orders-timeline.ts @@ -36,7 +36,7 @@ export const ordersTimeline = (_env: Env) => annotations: { readOnlyHint: true }, execute: async ({ runtimeContext }) => { const env = runtimeContext.env as Env; - const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT?.state); + const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT); assertValidCredentials(creds, TOOL_ID); const timezone = creds.timezone ?? DEFAULT_STORE_TIMEZONE; diff --git a/magento/server/tools/custom/status-breakdown.ts b/magento/server/tools/custom/status-breakdown.ts index 066004e9..54e33750 100644 --- a/magento/server/tools/custom/status-breakdown.ts +++ b/magento/server/tools/custom/status-breakdown.ts @@ -48,7 +48,7 @@ export const statusBreakdown = (_env: Env) => annotations: { readOnlyHint: true }, execute: async ({ context, runtimeContext }) => { const env = runtimeContext.env as Env; - const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT?.state); + const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT); assertValidCredentials(creds, TOOL_ID); const timezone = creds.timezone ?? DEFAULT_STORE_TIMEZONE; diff --git a/magento/server/tools/custom/top-products.ts b/magento/server/tools/custom/top-products.ts index ae0dd73d..fe47d634 100644 --- a/magento/server/tools/custom/top-products.ts +++ b/magento/server/tools/custom/top-products.ts @@ -62,7 +62,7 @@ export const topProducts = (_env: Env) => annotations: { readOnlyHint: true }, execute: async ({ context, runtimeContext }) => { const env = runtimeContext.env as Env; - const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT?.state); + const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT); assertValidCredentials(creds, TOOL_ID); const timezone = creds.timezone ?? DEFAULT_STORE_TIMEZONE; diff --git a/magento/server/tools/registry.ts b/magento/server/tools/registry.ts index eafe85e1..9ca3521b 100644 --- a/magento/server/tools/registry.ts +++ b/magento/server/tools/registry.ts @@ -71,7 +71,7 @@ function createSearchTool(config: { annotations: { readOnlyHint: true }, execute: async ({ context, runtimeContext }) => { const env = runtimeContext.env as Env; - const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT?.state); + const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT); assertValidCredentials(creds, config.id); const params = buildSearchCriteriaParams({ @@ -115,7 +115,7 @@ function createGetTool>(config: { annotations: { readOnlyHint: true }, execute: async ({ context, runtimeContext }) => { const env = runtimeContext.env as Env; - const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT?.state); + const creds = resolveCredentials(env.MESH_REQUEST_CONTEXT); assertValidCredentials(creds, config.id); const input = context as z.infer; diff --git a/magento/server/types/env.ts b/magento/server/types/env.ts index 38fdfbd2..85b71a56 100644 --- a/magento/server/types/env.ts +++ b/magento/server/types/env.ts @@ -10,11 +10,6 @@ export const StateSchema = z.object({ .describe( "Magento store base URL, e.g. https://loja.granado.com.br (no trailing slash, no /rest)", ), - apiToken: z - .string() - .describe( - "Magento integration access token, sent as Authorization: Bearer", - ), storeCode: z .string() .optional()