@dropins/mcp 0.1.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.
Files changed (103) hide show
  1. package/LICENSE.md +127 -0
  2. package/README.md +314 -0
  3. package/dist/common/project-reader.d.ts +55 -0
  4. package/dist/common/project-reader.js +173 -0
  5. package/dist/common/registry-loader.d.ts +101 -0
  6. package/dist/common/registry-loader.js +386 -0
  7. package/dist/common/response-handling.d.ts +12 -0
  8. package/dist/common/response-handling.js +21 -0
  9. package/dist/common/sanitize.d.ts +8 -0
  10. package/dist/common/sanitize.js +45 -0
  11. package/dist/common/synonyms.d.ts +9 -0
  12. package/dist/common/synonyms.js +127 -0
  13. package/dist/common/telemetry.d.ts +14 -0
  14. package/dist/common/telemetry.js +54 -0
  15. package/dist/common/types.d.ts +308 -0
  16. package/dist/common/types.js +1 -0
  17. package/dist/common/version.d.ts +2 -0
  18. package/dist/common/version.js +14 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +136 -0
  21. package/dist/operations/analyze-project.d.ts +13 -0
  22. package/dist/operations/analyze-project.js +125 -0
  23. package/dist/operations/check-block-health.d.ts +19 -0
  24. package/dist/operations/check-block-health.js +1149 -0
  25. package/dist/operations/check-config.d.ts +13 -0
  26. package/dist/operations/check-config.js +228 -0
  27. package/dist/operations/explain-event-flow.d.ts +16 -0
  28. package/dist/operations/explain-event-flow.js +218 -0
  29. package/dist/operations/get-upgrade-diff.d.ts +13 -0
  30. package/dist/operations/get-upgrade-diff.js +144 -0
  31. package/dist/operations/list-api-functions.d.ts +13 -0
  32. package/dist/operations/list-api-functions.js +53 -0
  33. package/dist/operations/list-containers.d.ts +13 -0
  34. package/dist/operations/list-containers.js +44 -0
  35. package/dist/operations/list-design-tokens.d.ts +13 -0
  36. package/dist/operations/list-design-tokens.js +47 -0
  37. package/dist/operations/list-events.d.ts +16 -0
  38. package/dist/operations/list-events.js +39 -0
  39. package/dist/operations/list-graphql-queries.d.ts +19 -0
  40. package/dist/operations/list-graphql-queries.js +84 -0
  41. package/dist/operations/list-i18n-keys.d.ts +19 -0
  42. package/dist/operations/list-i18n-keys.js +105 -0
  43. package/dist/operations/list-models.d.ts +16 -0
  44. package/dist/operations/list-models.js +80 -0
  45. package/dist/operations/list-slots.d.ts +16 -0
  46. package/dist/operations/list-slots.js +81 -0
  47. package/dist/operations/scaffold-block.d.ts +31 -0
  48. package/dist/operations/scaffold-block.js +331 -0
  49. package/dist/operations/scaffold-extension.d.ts +28 -0
  50. package/dist/operations/scaffold-extension.js +346 -0
  51. package/dist/operations/scaffold-slot.d.ts +22 -0
  52. package/dist/operations/scaffold-slot.js +189 -0
  53. package/dist/operations/search-commerce-docs.d.ts +16 -0
  54. package/dist/operations/search-commerce-docs.js +101 -0
  55. package/dist/operations/search-docs.d.ts +23 -0
  56. package/dist/operations/search-docs.js +298 -0
  57. package/dist/operations/suggest-event-handler.d.ts +16 -0
  58. package/dist/operations/suggest-event-handler.js +175 -0
  59. package/dist/operations/suggest-slot-implementation.d.ts +19 -0
  60. package/dist/operations/suggest-slot-implementation.js +183 -0
  61. package/dist/registry/api-functions.json +3045 -0
  62. package/dist/registry/block-patterns.json +78 -0
  63. package/dist/registry/containers.json +2003 -0
  64. package/dist/registry/design-tokens.json +577 -0
  65. package/dist/registry/docs/boilerplate.json +55 -0
  66. package/dist/registry/docs/dropins-all.json +97 -0
  67. package/dist/registry/docs/dropins-b2b.json +607 -0
  68. package/dist/registry/docs/dropins-cart.json +163 -0
  69. package/dist/registry/docs/dropins-checkout.json +193 -0
  70. package/dist/registry/docs/dropins-order.json +139 -0
  71. package/dist/registry/docs/dropins-payment-services.json +73 -0
  72. package/dist/registry/docs/dropins-personalization.json +67 -0
  73. package/dist/registry/docs/dropins-product-details.json +139 -0
  74. package/dist/registry/docs/dropins-product-discovery.json +85 -0
  75. package/dist/registry/docs/dropins-recommendations.json +67 -0
  76. package/dist/registry/docs/dropins-user-account.json +121 -0
  77. package/dist/registry/docs/dropins-user-auth.json +103 -0
  78. package/dist/registry/docs/dropins-wishlist.json +85 -0
  79. package/dist/registry/docs/get-started.json +85 -0
  80. package/dist/registry/docs/how-tos.json +19 -0
  81. package/dist/registry/docs/index.json +139 -0
  82. package/dist/registry/docs/licensing.json +19 -0
  83. package/dist/registry/docs/merchants.json +523 -0
  84. package/dist/registry/docs/resources.json +13 -0
  85. package/dist/registry/docs/sdk.json +139 -0
  86. package/dist/registry/docs/setup.json +145 -0
  87. package/dist/registry/docs/troubleshooting.json +19 -0
  88. package/dist/registry/events.json +2200 -0
  89. package/dist/registry/examples/index.json +19 -0
  90. package/dist/registry/examples/storefront-checkout.json +377 -0
  91. package/dist/registry/examples/storefront-quote-management.json +49 -0
  92. package/dist/registry/extensions.json +272 -0
  93. package/dist/registry/graphql.json +3469 -0
  94. package/dist/registry/i18n.json +1873 -0
  95. package/dist/registry/models.json +1001 -0
  96. package/dist/registry/sdk.json +2357 -0
  97. package/dist/registry/slots.json +2270 -0
  98. package/dist/registry/tools-components.json +595 -0
  99. package/dist/resources/guides.d.ts +7 -0
  100. package/dist/resources/guides.js +625 -0
  101. package/dist/resources/handlers.d.ts +31 -0
  102. package/dist/resources/handlers.js +322 -0
  103. package/package.json +47 -0
@@ -0,0 +1,346 @@
1
+ import { z } from "zod";
2
+ import { formatSuccessResponse, formatExceptionResponse, } from "../common/response-handling.js";
3
+ import { getExtensionRegistry } from "../common/registry-loader.js";
4
+ import { projectDirGuard } from "../common/project-reader.js";
5
+ import { validateExtensionId, validateUrl, escapeJsString, sanitizeForComment, } from "../common/sanitize.js";
6
+ export const ScaffoldExtensionSchema = z.object({
7
+ extensionName: z
8
+ .string()
9
+ .describe('Human-readable name (e.g. "Stripe Payment")'),
10
+ extensionId: z
11
+ .string()
12
+ .describe('Unique ID (e.g. "stripe-payment"). Used as folder name and id field.'),
13
+ hooks: z
14
+ .array(z.string())
15
+ .describe('Hook names to implement (e.g. ["checkout/payment-methods", "checkout/validate", "checkout/place-order"])'),
16
+ externalScripts: z
17
+ .array(z.string())
18
+ .optional()
19
+ .describe('External script URLs to load (e.g. ["https://js.stripe.com/v3/"])'),
20
+ externalStyles: z
21
+ .array(z.string())
22
+ .optional()
23
+ .describe("External stylesheet URLs to load"),
24
+ projectDir: z
25
+ .string()
26
+ .describe("Absolute path to the merchant storefront project root"),
27
+ });
28
+ function generateHookStub(hook) {
29
+ const contextFields = Object.entries(hook.contextShape ?? {})
30
+ .map(([key, type]) => ` * ${key}: ${type}`)
31
+ .join("\n");
32
+ const contextDoc = contextFields ? `\n * Context:\n${contextFields}` : "";
33
+ switch (hook.name) {
34
+ case "checkout/validate":
35
+ return ` /**
36
+ * Validation hook — set context.isValid = false to block order placement${contextDoc}
37
+ */
38
+ 'checkout/validate': async ({ context }) => {
39
+ const { code } = context;
40
+
41
+ // Only handle your payment method
42
+ if (code !== 'YOUR_PAYMENT_CODE') return;
43
+
44
+ // Perform custom validation
45
+ const isValid = true; // Replace with actual validation
46
+ if (!isValid) {
47
+ context.isValid = false;
48
+ return;
49
+ }
50
+ },`;
51
+ case "checkout/payment-methods":
52
+ return ` /**
53
+ * Payment methods hook — add your payment method to context.paymentMethods${contextDoc}
54
+ */
55
+ 'checkout/payment-methods': async ({ context }) => {
56
+ context.paymentMethods.YOUR_PAYMENT_CODE = {
57
+ autoSync: false,
58
+ render: (ctx) => {
59
+ const container = document.createElement('div');
60
+ container.className = 'custom-payment-method';
61
+
62
+ const title = document.createElement('h3');
63
+ title.textContent = 'Your Payment Method';
64
+
65
+ const description = document.createElement('p');
66
+ description.textContent = 'Payment method description here.';
67
+
68
+ container.appendChild(title);
69
+ container.appendChild(description);
70
+ ctx.replaceHTML(container);
71
+ },
72
+ };
73
+ },`;
74
+ case "checkout/place-order":
75
+ return ` /**
76
+ * Place order hook — handle payment processing${contextDoc}
77
+ */
78
+ 'checkout/place-order': async ({ context }) => {
79
+ const { cartId, code } = context;
80
+
81
+ // Only handle your payment method
82
+ if (code !== 'YOUR_PAYMENT_CODE') return;
83
+
84
+ // Prevent default order placement
85
+ context.preventDefault = true;
86
+
87
+ // Process payment with your provider
88
+ // const paymentResult = await processPayment(cartId);
89
+
90
+ // Place order via API
91
+ // const { placeOrder } = await import('@dropins/storefront-order/api.js');
92
+ // await placeOrder(cartId);
93
+ },`;
94
+ case "checkout/payment-response":
95
+ return ` /**
96
+ * Payment response hook — runs before checkout renders, detect redirect returns here${contextDoc}
97
+ */
98
+ 'checkout/payment-response': async ({ context }) => {
99
+ const { block, shouldExit } = context;
100
+
101
+ // Detect a returning payment redirect (e.g. check URL params or session state)
102
+ const isReturningFromRedirect = false; // replace with your detection logic
103
+
104
+ if (isReturningFromRedirect) {
105
+ context.shouldExit = true; // stop normal checkout flow
106
+ // Handle the gateway response here
107
+ }
108
+ },`;
109
+ case "checkout/address-form-render":
110
+ return ` /**
111
+ * Address form render hook — wrap context.render() to augment address form behavior${contextDoc}
112
+ */
113
+ 'checkout/address-form-render': async ({ context }) => {
114
+ const { container, addressType, render } = context;
115
+
116
+ // addressType is 'shipping' or 'billing'
117
+ // Wrap the default render to inject custom behavior before/after
118
+ context.render = (props) => {
119
+ render(props); // call the original render first
120
+ // Add custom fields or post-render logic here
121
+ // const customField = document.createElement('input');
122
+ // container.appendChild(customField);
123
+ };
124
+ },`;
125
+ default:
126
+ return ` /**
127
+ * ${hook.name} hook
128
+ * ${hook.description ?? ""}${contextDoc}
129
+ */
130
+ '${hook.name}': async ({ context }) => {
131
+ // Implement hook logic here
132
+ },`;
133
+ }
134
+ }
135
+ function generateExtensionFile(extensionName, extensionId, hookDefs, externalScripts, externalStyles) {
136
+ const hookStubs = hookDefs.map(generateHookStub).join("\n\n");
137
+ const scriptsProp = externalScripts?.length
138
+ ? `\n externalScripts: [\n ${externalScripts.map((s) => `'${escapeJsString(s)}'`).join(",\n ")},\n ],`
139
+ : "";
140
+ const stylesProp = externalStyles?.length
141
+ ? `\n externalStyles: [\n ${externalStyles.map((s) => `'${escapeJsString(s)}'`).join(",\n ")},\n ],`
142
+ : "";
143
+ return `/**
144
+ * ${sanitizeForComment(extensionName)}
145
+ *
146
+ * Checkout extension that implements: ${hookDefs.map((h) => h.name).join(", ")}
147
+ */
148
+
149
+ export default {
150
+ id: '${escapeJsString(extensionId)}',
151
+ name: '${escapeJsString(extensionName)}',${scriptsProp}${stylesProp}
152
+
153
+ hooks: {
154
+ ${hookStubs}
155
+ },
156
+ };
157
+ `;
158
+ }
159
+ function generateExtensionsIndex(extensionId) {
160
+ return `/**
161
+ * Commerce Checkout Extensions
162
+ *
163
+ * Import and register all extensions here.
164
+ * ADD new extensions — do not replace this file.
165
+ */
166
+
167
+ import ${toCamelCase(extensionId)} from './${extensionId}/${extensionId}.js';
168
+
169
+ export default [
170
+ ${toCamelCase(extensionId)},
171
+ ];
172
+ `;
173
+ }
174
+ function generateIndexMergeSnippet(extensionId) {
175
+ return {
176
+ importLine: `import ${toCamelCase(extensionId)} from './${extensionId}/${extensionId}.js';`,
177
+ exportEntry: ` ${toCamelCase(extensionId)},`,
178
+ };
179
+ }
180
+ function generateExtensionManager() {
181
+ return `/**
182
+ * Extension Manager
183
+ *
184
+ * Singleton manager that loads extensions and executes hooks.
185
+ */
186
+
187
+ function loadScript(src) {
188
+ return new Promise((resolve, reject) => {
189
+ if (document.querySelector(\`script[src="\${src}"]\`)) {
190
+ resolve();
191
+ return;
192
+ }
193
+ const script = document.createElement('script');
194
+ script.src = src;
195
+ script.onload = resolve;
196
+ script.onerror = () => reject(new Error(\`Failed to load script: \${src}\`));
197
+ document.head.appendChild(script);
198
+ });
199
+ }
200
+
201
+ function loadStyle(href) {
202
+ return new Promise((resolve) => {
203
+ if (document.querySelector(\`link[href="\${href}"]\`)) {
204
+ resolve();
205
+ return;
206
+ }
207
+ const link = document.createElement('link');
208
+ link.rel = 'stylesheet';
209
+ link.href = href;
210
+ link.onload = resolve;
211
+ document.head.appendChild(link);
212
+ });
213
+ }
214
+
215
+ let extensionManagerInstance = null;
216
+ let initializationPromise = null;
217
+
218
+ async function createExtensionManager() {
219
+ const extensions = [];
220
+
221
+ try {
222
+ const extensionsModule = await import('./extensions/index.js');
223
+ extensions.push(...(extensionsModule.default || []));
224
+
225
+ const scripts = extensions.flatMap((ext) => ext.externalScripts || []);
226
+ const styles = extensions.flatMap((ext) => ext.externalStyles || []);
227
+
228
+ await Promise.all([
229
+ ...scripts.map(loadScript),
230
+ ...styles.map(loadStyle),
231
+ ]);
232
+ } catch (e) {
233
+ console.error('[ExtensionManager] Failed to load extensions:', e.message);
234
+ }
235
+
236
+ return {
237
+ async executeHook(hookName, context = {}) {
238
+ for (const ext of extensions) {
239
+ if (ext.hooks?.[hookName]) {
240
+ try {
241
+ await ext.hooks[hookName]({ context });
242
+ } catch (error) {
243
+ console.error(\`[ExtensionManager] Error in \${hookName} for \${ext.name}:\`, error);
244
+ throw error;
245
+ }
246
+ }
247
+ }
248
+ },
249
+ };
250
+ }
251
+
252
+ /** Returns the extension manager boilerplate code for loading and executing hooks. */
253
+ export async function getExtensionManager() {
254
+ if (extensionManagerInstance) return extensionManagerInstance;
255
+ if (!initializationPromise) initializationPromise = createExtensionManager();
256
+ extensionManagerInstance = await initializationPromise;
257
+ return extensionManagerInstance;
258
+ }
259
+ `;
260
+ }
261
+ function toCamelCase(str) {
262
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
263
+ }
264
+ export async function scaffoldExtension(params) {
265
+ try {
266
+ const extIdError = validateExtensionId(params.extensionId);
267
+ if (extIdError) {
268
+ return formatSuccessResponse("Invalid extension ID", {
269
+ error: extIdError,
270
+ });
271
+ }
272
+ if (params.externalScripts) {
273
+ for (const url of params.externalScripts) {
274
+ const urlError = validateUrl(url, "external script URL");
275
+ if (urlError) {
276
+ return formatSuccessResponse("Invalid external script URL", {
277
+ error: urlError,
278
+ });
279
+ }
280
+ }
281
+ }
282
+ if (params.externalStyles) {
283
+ for (const url of params.externalStyles) {
284
+ const urlError = validateUrl(url, "external stylesheet URL");
285
+ if (urlError) {
286
+ return formatSuccessResponse("Invalid external stylesheet URL", {
287
+ error: urlError,
288
+ });
289
+ }
290
+ }
291
+ }
292
+ const guard = projectDirGuard(params.projectDir);
293
+ if (guard)
294
+ return guard;
295
+ const registry = getExtensionRegistry();
296
+ const availableHooks = registry.hooks.map((h) => h.name);
297
+ const invalidHooks = params.hooks.filter((h) => !availableHooks.includes(h));
298
+ if (invalidHooks.length > 0) {
299
+ return formatSuccessResponse("Invalid hook names", {
300
+ invalidHooks,
301
+ availableHooks,
302
+ });
303
+ }
304
+ const hookDefs = params.hooks
305
+ .map((hookName) => registry.hooks.find((h) => h.name === hookName))
306
+ .filter((h) => h !== undefined);
307
+ const extensionFile = generateExtensionFile(params.extensionName, params.extensionId, hookDefs, params.externalScripts, params.externalStyles);
308
+ const indexFile = generateExtensionsIndex(params.extensionId);
309
+ const indexMergeSnippet = generateIndexMergeSnippet(params.extensionId);
310
+ const managerFile = generateExtensionManager();
311
+ const blockBase = "blocks/commerce-checkout";
312
+ const extDir = `${blockBase}/extensions/${params.extensionId}`;
313
+ const extFile = `${extDir}/${params.extensionId}.js`;
314
+ const extIndex = `${blockBase}/extensions/index.js`;
315
+ const extManager = `${blockBase}/extensions.js`;
316
+ return formatSuccessResponse(`Extension "${params.extensionName}" scaffolded successfully`, {
317
+ files: {
318
+ [extFile]: extensionFile,
319
+ [extIndex]: indexFile,
320
+ [extManager]: managerFile,
321
+ },
322
+ indexMergeSnippet,
323
+ instructions: [
324
+ `Create directory: ${extDir}/`,
325
+ `Write extension file: ${extFile}`,
326
+ `If ${extIndex} does NOT exist: write the full indexFile content above`,
327
+ `If ${extIndex} already EXISTS: add only indexMergeSnippet.importLine near the top and indexMergeSnippet.exportEntry inside the export default array — do NOT replace the file`,
328
+ `Write extension manager (if not exists): ${extManager}`,
329
+ "Replace YOUR_PAYMENT_CODE with your actual payment method code",
330
+ params.externalScripts?.length
331
+ ? `External scripts will be auto-loaded: ${params.externalScripts.join(", ")}`
332
+ : "No external scripts configured",
333
+ "Import getExtensionManager in your checkout block to use extensions",
334
+ 'Call extensionManager.executeHook("hookName", context) at the appropriate lifecycle points',
335
+ ],
336
+ hooksSummary: hookDefs.map((h) => ({
337
+ name: h?.name,
338
+ description: h?.description,
339
+ whenFired: h?.whenFired,
340
+ })),
341
+ });
342
+ }
343
+ catch (error) {
344
+ return formatExceptionResponse(error, "scaffolding extension");
345
+ }
346
+ }
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ export declare const ScaffoldSlotSchema: z.ZodObject<{
3
+ containerName: z.ZodString;
4
+ slotName: z.ZodString;
5
+ dropin: z.ZodOptional<z.ZodString>;
6
+ projectDir: z.ZodString;
7
+ }, "strip", z.ZodTypeAny, {
8
+ projectDir: string;
9
+ containerName: string;
10
+ slotName: string;
11
+ dropin?: string | undefined;
12
+ }, {
13
+ projectDir: string;
14
+ containerName: string;
15
+ slotName: string;
16
+ dropin?: string | undefined;
17
+ }>;
18
+ export declare function scaffoldSlot(params: z.infer<typeof ScaffoldSlotSchema>): Promise<{
19
+ success: boolean;
20
+ message: string;
21
+ data: unknown;
22
+ }>;
@@ -0,0 +1,189 @@
1
+ import { z } from "zod";
2
+ import { formatSuccessResponse, formatExceptionResponse, } from "../common/response-handling.js";
3
+ import { getSlotRegistry, getContainerRegistry, } from "../common/registry-loader.js";
4
+ import { projectDirGuard } from "../common/project-reader.js";
5
+ export const ScaffoldSlotSchema = z.object({
6
+ containerName: z
7
+ .string()
8
+ .describe('Container name (e.g. "MiniCart", "ShippingMethods")'),
9
+ slotName: z
10
+ .string()
11
+ .describe('Slot name to scaffold (e.g. "Thumbnail", "ShippingMethodItem")'),
12
+ dropin: z
13
+ .string()
14
+ .optional()
15
+ .describe('Dropin package name (e.g. "storefront-cart"). Provide to resolve ambiguity when the same slot name exists in multiple dropins.'),
16
+ projectDir: z
17
+ .string()
18
+ .describe("Absolute path to the merchant storefront project root"),
19
+ });
20
+ function extractContextProps(contextType) {
21
+ if (!contextType || contextType === "DefaultSlotContext")
22
+ return [];
23
+ if (!contextType.includes("{"))
24
+ return [];
25
+ const matches = [...contextType.matchAll(/^\s*(\w+)\s*:/gm)];
26
+ return matches.map((m) => m[1]);
27
+ }
28
+ function toKebabCase(str) {
29
+ return str.replace(/([A-Z])/g, (_, c, i) => (i > 0 ? "-" : "") + c.toLowerCase());
30
+ }
31
+ function buildSlotCodeParts(slot, registry) {
32
+ const contextType = slot.contextType ?? "DefaultSlotContext";
33
+ const customMethods = slot.customMethods ?? [];
34
+ const defaultMethods = registry.defaultSlotMethods ?? {};
35
+ const contextProps = extractContextProps(contextType);
36
+ const allMethods = [
37
+ ...Object.entries(defaultMethods).map(([name, desc]) => ({ name, desc })),
38
+ ...customMethods
39
+ .filter((m) => !(m in defaultMethods))
40
+ .map((m) => ({ name: m, desc: "" })),
41
+ ];
42
+ const allMethodNames = allMethods.map(({ name }) => name);
43
+ const methodComments = allMethods
44
+ .map(({ name, desc }) => desc ? ` // ctx.${name}(...) - ${desc}` : ` // ctx.${name}(...)`)
45
+ .join("\n");
46
+ const contextDestructure = contextProps.length > 0
47
+ ? ` const { ${contextProps.join(", ")} } = ctx;\n`
48
+ : "";
49
+ if (customMethods.includes("appendAgreement")) {
50
+ return {
51
+ contextProps,
52
+ allMethodNames,
53
+ slotCode: `${slot.name}: (ctx) => {
54
+ ctx.appendAgreement(() => ({
55
+ name: 'custom-agreement',
56
+ mode: 'manual',
57
+ translationId: 'Custom.Agreement.Label',
58
+ }));
59
+ },`,
60
+ };
61
+ }
62
+ return {
63
+ contextProps,
64
+ allMethodNames,
65
+ slotCode: `${slot.name}: (ctx) => {
66
+ // Available methods:
67
+ ${methodComments}
68
+ ${contextDestructure}
69
+ const element = document.createElement('div');
70
+ element.className = 'custom-${toKebabCase(slot.name)}';
71
+
72
+ // Build your custom content here
73
+ element.textContent = 'Custom content';
74
+
75
+ ctx.appendChild(element);
76
+ },`,
77
+ };
78
+ }
79
+ function generateScaffold(slotCode, match) {
80
+ return `// 1. Imports
81
+ import { render as provider } from '${match.renderImport}';
82
+ import ${match.containerName} from '${match.containerImportPath}';
83
+
84
+ // 2. Render the container with the slot
85
+ const container = document.querySelector('.block-placeholder');
86
+ if (container) {
87
+ provider.render(${match.containerName}, {
88
+ slots: {
89
+ ${slotCode}
90
+ },
91
+ })(container);
92
+ }`;
93
+ }
94
+ export async function scaffoldSlot(params) {
95
+ try {
96
+ const guard = projectDirGuard(params.projectDir);
97
+ if (guard)
98
+ return guard;
99
+ const slotRegistry = getSlotRegistry();
100
+ const containerRegistry = getContainerRegistry();
101
+ const matches = [];
102
+ const searchDropins = params.dropin
103
+ ? [params.dropin]
104
+ : Object.keys(slotRegistry.dropins);
105
+ const containerLower = params.containerName.toLowerCase();
106
+ const slotLower = params.slotName.toLowerCase();
107
+ for (const dropinName of searchDropins) {
108
+ const dropinSlotData = slotRegistry.dropins[dropinName];
109
+ if (!dropinSlotData)
110
+ continue;
111
+ for (const [cName, cData] of Object.entries(dropinSlotData.containers)) {
112
+ if (cName.toLowerCase() !== containerLower)
113
+ continue;
114
+ const slot = cData.slots.find((s) => s.name.toLowerCase() === slotLower);
115
+ if (!slot)
116
+ continue;
117
+ const dropinContainerData = containerRegistry.dropins[dropinName];
118
+ const containerEntry = dropinContainerData?.containers.find((c) => c.name.toLowerCase() === containerLower);
119
+ matches.push({
120
+ dropin: dropinName,
121
+ containerName: cName,
122
+ slot,
123
+ renderImport: dropinContainerData?.renderImport ??
124
+ `@dropins/${dropinName}/render.js`,
125
+ containerImportPath: containerEntry?.importPath ??
126
+ `@dropins/${dropinName}/containers/${cName}.js`,
127
+ });
128
+ }
129
+ }
130
+ if (matches.length === 0) {
131
+ const knownContainers = Object.entries(slotRegistry.dropins)
132
+ .flatMap(([d, data]) => Object.keys(data.containers).map((c) => `${d} → ${c}`))
133
+ .slice(0, 12);
134
+ return formatSuccessResponse(`Slot "${params.slotName}" not found in container "${params.containerName}"`, {
135
+ error: "Slot not found",
136
+ searched: params.dropin ?? "all dropins",
137
+ hint: `Use list_slots to browse available slots. Sample known containers: ${knownContainers.join(", ")}`,
138
+ });
139
+ }
140
+ if (matches.length > 1) {
141
+ return formatSuccessResponse(`Slot "${params.slotName}" exists in multiple dropins - specify the dropin parameter`, {
142
+ ambiguousMatches: matches.map((m) => ({
143
+ dropin: m.dropin,
144
+ containerName: m.containerName,
145
+ slotName: m.slot.name,
146
+ })),
147
+ });
148
+ }
149
+ const match = matches[0];
150
+ const { contextProps, allMethodNames, slotCode } = buildSlotCodeParts(match.slot, slotRegistry);
151
+ const codeScaffold = generateScaffold(slotCode, match);
152
+ return formatSuccessResponse(`Slot "${match.slot.name}" scaffolded for ${match.containerName} (${match.dropin})`, {
153
+ dropin: match.dropin,
154
+ containerName: match.containerName,
155
+ slotName: match.slot.name,
156
+ description: match.slot.description || "(no description)",
157
+ contextType: match.slot.contextType,
158
+ contextProps: contextProps.length > 0
159
+ ? contextProps
160
+ : match.slot.contextType &&
161
+ match.slot.contextType !== "DefaultSlotContext"
162
+ ? [
163
+ `named type: ${match.slot.contextType} - use list_slots to inspect its shape`,
164
+ ]
165
+ : ["no typed props - default slot context"],
166
+ availableMethods: allMethodNames,
167
+ customMethods: match.slot.customMethods ?? [],
168
+ isDynamic: match.slot.dynamic,
169
+ codeScaffold,
170
+ instructions: [
171
+ `Import render from: ${match.renderImport} (aliased as provider)`,
172
+ `Import container from: ${match.containerImportPath}`,
173
+ "Pass the slot function inside the slots prop of provider.render(Container, { slots: {...} })(element)",
174
+ contextProps.length > 0
175
+ ? `Destructure context props from ctx: { ${contextProps.join(", ")} }`
176
+ : "Use ctx.appendChild(element) to inject content into the slot",
177
+ match.slot.customMethods?.length
178
+ ? `Extra methods on ctx: ${match.slot.customMethods.join(", ")}`
179
+ : "Only default slot methods are available on ctx",
180
+ match.slot.dynamic
181
+ ? "Dynamic slot: this function is called once per item in a list (e.g. per product, per shipping method)"
182
+ : "Static slot: this function is called once per container instance",
183
+ ],
184
+ });
185
+ }
186
+ catch (error) {
187
+ return formatExceptionResponse(error, "scaffolding slot");
188
+ }
189
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ export declare const SearchCommerceDocsSchema: z.ZodObject<{
3
+ query: z.ZodString;
4
+ maxResults: z.ZodOptional<z.ZodNumber>;
5
+ }, "strip", z.ZodTypeAny, {
6
+ query: string;
7
+ maxResults?: number | undefined;
8
+ }, {
9
+ query: string;
10
+ maxResults?: number | undefined;
11
+ }>;
12
+ export declare function searchCommerceDocs(params: z.infer<typeof SearchCommerceDocsSchema>): Promise<{
13
+ success: boolean;
14
+ message: string;
15
+ data: unknown;
16
+ }>;
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ import { formatSuccessResponse, formatExceptionResponse, } from "../common/response-handling.js";
5
+ const execFileAsync = promisify(execFile);
6
+ const RAG_ENDPOINTS = {
7
+ prod: "https://commerce-docs-prod-endpoint-d0ctgyebe7bec8e6.a02.azurefd.net/api/query",
8
+ stage: "https://commerce-docs-dev-endpoint-hbe8ceethxh9d2g3.a02.azurefd.net/api/query",
9
+ };
10
+ export const SearchCommerceDocsSchema = z.object({
11
+ query: z
12
+ .string()
13
+ .describe('Natural language search query (e.g. "how to add a custom payment method", "CORS configuration for storefront")'),
14
+ maxResults: z
15
+ .number()
16
+ .optional()
17
+ .describe("Maximum number of results to return (default: 5)"),
18
+ });
19
+ async function getImsToken() {
20
+ const { stdout } = await execFileAsync("aio", ["config", "get", "ims.contexts.cli.access_token.token"], { timeout: 10000 });
21
+ const token = stdout.trim();
22
+ if (!token) {
23
+ throw new Error("No IMS token returned. Run: aio auth login");
24
+ }
25
+ return token;
26
+ }
27
+ async function getAioEnvironment() {
28
+ try {
29
+ const { stdout } = await execFileAsync("aio", ["config", "get", "cli.env"], { timeout: 5000 });
30
+ if (stdout.trim() === "stage")
31
+ return "stage";
32
+ }
33
+ catch {
34
+ }
35
+ return "prod";
36
+ }
37
+ export async function searchCommerceDocs(params) {
38
+ try {
39
+ const [token, env] = await Promise.all([
40
+ getImsToken(),
41
+ getAioEnvironment(),
42
+ ]);
43
+ const endpoint = RAG_ENDPOINTS[env];
44
+ const maxResults = params.maxResults ?? 5;
45
+ const controller = new AbortController();
46
+ const timeout = setTimeout(() => controller.abort(), 30000);
47
+ let response;
48
+ try {
49
+ response = await fetch(endpoint, {
50
+ method: "POST",
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ Authorization: `Bearer ${token}`,
54
+ },
55
+ body: JSON.stringify({
56
+ query: params.query,
57
+ count: maxResults,
58
+ }),
59
+ signal: controller.signal,
60
+ });
61
+ }
62
+ finally {
63
+ clearTimeout(timeout);
64
+ }
65
+ if (!response.ok) {
66
+ if (response.status === 401) {
67
+ throw new Error("IMS token is expired or invalid. Run: aio auth login");
68
+ }
69
+ if (response.status === 429) {
70
+ throw new Error("Rate limit exceeded. Try again later.");
71
+ }
72
+ throw new Error(`Documentation service returned HTTP ${response.status}`);
73
+ }
74
+ const payload = (await response.json());
75
+ if (!payload.success || !payload.results) {
76
+ throw new Error("Invalid response from documentation service");
77
+ }
78
+ const results = payload.results.map((doc, i) => ({
79
+ rank: i + 1,
80
+ source: doc.metadata?.source ?? doc.source ?? "Unknown",
81
+ content: doc.content ?? "",
82
+ score: doc.score,
83
+ metadata: doc.metadata ?? {},
84
+ }));
85
+ return formatSuccessResponse(`Found ${results.length} result(s) for: "${params.query}"`, {
86
+ query: params.query,
87
+ index: payload.index,
88
+ indexSelection: payload.indexSelection ?? "backend-selected",
89
+ confidence: payload.confidence,
90
+ results,
91
+ note: "For structured dropin API data (slots, events, containers, GraphQL), use the dedicated registry tools instead.",
92
+ });
93
+ }
94
+ catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ if (message.includes("ENOENT") || message.includes("not found")) {
97
+ return formatExceptionResponse(new Error("The aio CLI is required for documentation search. Install it with: npm install -g @adobe/aio-cli && aio auth login"), "searching commerce documentation");
98
+ }
99
+ return formatExceptionResponse(error, "searching commerce documentation");
100
+ }
101
+ }