magector 2.10.0 → 2.11.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 (2) hide show
  1. package/package.json +5 -5
  2. package/src/mcp-server.js +204 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
5
  "type": "module",
6
6
  "main": "src/mcp-server.js",
@@ -33,10 +33,10 @@
33
33
  "ruvector": "^0.1.96"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@magector/cli-darwin-arm64": "2.10.0",
37
- "@magector/cli-linux-x64": "2.10.0",
38
- "@magector/cli-linux-arm64": "2.10.0",
39
- "@magector/cli-win32-x64": "2.10.0"
36
+ "@magector/cli-darwin-arm64": "2.11.0",
37
+ "@magector/cli-linux-x64": "2.11.0",
38
+ "@magector/cli-linux-arm64": "2.11.0",
39
+ "@magector/cli-win32-x64": "2.11.0"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/mcp-server.js CHANGED
@@ -4150,9 +4150,31 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
4150
4150
  required: ['pattern']
4151
4151
  }
4152
4152
  },
4153
+ {
4154
+ name: 'magento_trace_api',
4155
+ description: 'Trace a REST or GraphQL API endpoint from URL to implementation. Parses webapi.xml to find the service interface, resolves the DI preference to the concrete class, reads the execute/method body, and checks di.xml for constructor arguments. Returns the complete chain in one call.',
4156
+ inputSchema: {
4157
+ type: 'object',
4158
+ properties: {
4159
+ url: {
4160
+ type: 'string',
4161
+ description: 'REST API URL pattern to trace. Example: "/V1/orders/:orderId/items", "/V1/carts/mine/payment-information"'
4162
+ },
4163
+ interfaceName: {
4164
+ type: 'string',
4165
+ description: 'Alternative: service interface class name. Example: "ChangePaymentMethodInterface"'
4166
+ },
4167
+ method: {
4168
+ type: 'string',
4169
+ description: 'HTTP method (GET, PUT, POST, DELETE). Default: any.',
4170
+ enum: ['GET', 'PUT', 'POST', 'DELETE']
4171
+ }
4172
+ }
4173
+ }
4174
+ },
4153
4175
  {
4154
4176
  name: 'magento_read',
4155
- description: 'Read a file from the Magento codebase. Use in magento_batch to read multiple files in a single MCP call (e.g., grep finds 5 files → read all 5 in one batch). Supports line ranges for large files.',
4177
+ description: 'Read a file from the Magento codebase. Use in magento_batch to read multiple files in a single MCP call (e.g., grep finds 5 files → read all 5 in one batch). Supports line ranges and method extraction.',
4156
4178
  inputSchema: {
4157
4179
  type: 'object',
4158
4180
  properties: {
@@ -4167,6 +4189,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
4167
4189
  endLine: {
4168
4190
  type: 'number',
4169
4191
  description: 'Stop reading at this line number (inclusive). Default: end of file. Use with startLine for large files.'
4192
+ },
4193
+ methodName: {
4194
+ type: 'string',
4195
+ description: 'Extract only this method from the file (uses brace-counting). Returns the complete method body with line numbers. Much more token-efficient than reading the whole file. Example: "execute"'
4170
4196
  }
4171
4197
  },
4172
4198
  required: ['path']
@@ -4190,7 +4216,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4190
4216
  // These tools have filesystem/di.xml fallbacks — work without serve process
4191
4217
  'magento_find_class', 'magento_find_method', 'magento_find_plugin',
4192
4218
  'magento_find_observer', 'magento_find_di_wiring', 'magento_module_structure',
4193
- 'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep', 'magento_read'];
4219
+ 'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep', 'magento_read', 'magento_trace_api'];
4194
4220
  if (warmupInProgress && !indexFreeTools.includes(name)) {
4195
4221
  logToFile('REQ', `${name} → blocked (warmup: loading index)`);
4196
4222
  return {
@@ -6027,6 +6053,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6027
6053
  text = `File not found: ${a.path}`;
6028
6054
  break;
6029
6055
  }
6056
+ if (a.methodName) {
6057
+ const body = readFullMethodBody(filePath, a.methodName);
6058
+ if (!body) { text = `Method ${a.methodName} not found in ${a.path}`; break; }
6059
+ const fLines = fileContent.split('\n');
6060
+ let mLine = 0;
6061
+ for (let i = 0; i < fLines.length; i++) {
6062
+ if (fLines[i].includes(`function ${a.methodName}(`)) { mLine = i + 1; break; }
6063
+ }
6064
+ text = body.split('\n').map((line, i) => `${mLine + i}\t${line}`).join('\n');
6065
+ break;
6066
+ }
6030
6067
  const allLines = fileContent.split('\n');
6031
6068
  const s = Math.max((a.startLine || 1) - 1, 0);
6032
6069
  const e = a.endLine ? Math.min(a.endLine, allLines.length) : allLines.length;
@@ -6039,7 +6076,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6039
6076
  const include = a.include || '*.php';
6040
6077
  const maxRes = Math.min(a.maxResults || 30, 100);
6041
6078
  const batchCtx = a.context !== undefined ? a.context : 2;
6042
- const gArgs = ['-rn'];
6079
+ const gArgs = ['-rn', '-E'];
6043
6080
  if (a.ignoreCase) gArgs.push('-i');
6044
6081
  if (batchCtx > 0) gArgs.push('-C', String(batchCtx));
6045
6082
  for (const pat of include.split(',').map(p => p.trim())) gArgs.push('--include=' + pat);
@@ -6076,7 +6113,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6076
6113
  const include = args.include || '*.php';
6077
6114
  const maxResults = Math.min(args.maxResults || 50, 200);
6078
6115
  const ctxLines = args.context !== undefined ? args.context : 2;
6079
- const grepArgs = ['-rn'];
6116
+ const grepArgs = ['-rn', '-E'];
6080
6117
  if (args.ignoreCase) grepArgs.push('-i');
6081
6118
  if (ctxLines > 0) grepArgs.push('-C', String(ctxLines));
6082
6119
  // Support multiple include patterns (e.g., "*.{php,xml}")
@@ -6106,6 +6143,151 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6106
6143
  return { content: [{ type: 'text', text }] };
6107
6144
  }
6108
6145
 
6146
+ case 'magento_trace_api': {
6147
+ const root = config.magentoRoot;
6148
+ if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
6149
+ let text = '';
6150
+
6151
+ // 1. Find the endpoint in webapi.xml files
6152
+ const webapiFiles = await glob('**/etc/webapi.xml', { cwd: root, absolute: true, nodir: true });
6153
+ let matchedRoute = null;
6154
+ const searchUrl = args.url || '';
6155
+ const searchInterface = args.interfaceName || '';
6156
+ const searchMethod = args.method || '';
6157
+
6158
+ for (const wf of webapiFiles) {
6159
+ let wContent;
6160
+ try { wContent = readFileSync(wf, 'utf-8'); } catch { continue; }
6161
+ const relPath = wf.replace(root + '/', '');
6162
+
6163
+ const routeRegex = /<route\s+url="([^"]+)"\s+method="([^"]+)"[^>]*>([\s\S]*?)<\/route>/g;
6164
+ let rm;
6165
+ while ((rm = routeRegex.exec(wContent)) !== null) {
6166
+ const routeUrl = rm[1];
6167
+ const routeMethod = rm[2];
6168
+ const routeBody = rm[3];
6169
+
6170
+ const urlMatch = searchUrl ? routeUrl.includes(searchUrl) || searchUrl.includes(routeUrl) : false;
6171
+ const ifaceMatch = searchInterface ? routeBody.includes(searchInterface) : false;
6172
+ const methodMatch = searchMethod ? routeMethod === searchMethod : true;
6173
+
6174
+ if ((urlMatch || ifaceMatch) && methodMatch) {
6175
+ const serviceMatch = routeBody.match(/class="([^"]+)"\s+method="([^"]+)"/);
6176
+ if (serviceMatch) {
6177
+ matchedRoute = {
6178
+ url: routeUrl,
6179
+ httpMethod: routeMethod,
6180
+ serviceClass: serviceMatch[1],
6181
+ serviceMethod: serviceMatch[2],
6182
+ file: relPath
6183
+ };
6184
+ // Extract resource
6185
+ const resMatch = routeBody.match(/resource\s+ref="([^"]+)"/);
6186
+ if (resMatch) matchedRoute.acl = resMatch[1];
6187
+ break;
6188
+ }
6189
+ }
6190
+ }
6191
+ if (matchedRoute) break;
6192
+ }
6193
+
6194
+ if (!matchedRoute) {
6195
+ return { content: [{ type: 'text', text: `No API endpoint found matching url="${searchUrl}" interface="${searchInterface}"` }] };
6196
+ }
6197
+
6198
+ text += `## API Endpoint\n\n`;
6199
+ text += `- **URL:** \`${matchedRoute.httpMethod} ${matchedRoute.url}\`\n`;
6200
+ text += `- **Interface:** \`${matchedRoute.serviceClass}::${matchedRoute.serviceMethod}()\`\n`;
6201
+ text += `- **ACL:** \`${matchedRoute.acl || 'none'}\`\n`;
6202
+ text += `- **webapi.xml:** \`${matchedRoute.file}\`\n\n`;
6203
+
6204
+ // 2. Find DI preference (implementation)
6205
+ const diFiles = await getDiXmlFiles(root);
6206
+ const shortIface = matchedRoute.serviceClass.split('\\').pop();
6207
+ let implClass = null;
6208
+ let implFile = null;
6209
+
6210
+ for (const { content: diContent, relPath } of diFiles) {
6211
+ if (!diContent.includes(shortIface)) continue;
6212
+ const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
6213
+ let pm;
6214
+ while ((pm = prefRegex.exec(diContent)) !== null) {
6215
+ if (pm[1].includes(shortIface)) {
6216
+ implClass = pm[2];
6217
+ implFile = relPath;
6218
+ break;
6219
+ }
6220
+ }
6221
+ if (implClass) break;
6222
+ }
6223
+
6224
+ if (implClass) {
6225
+ text += `## Implementation\n\n`;
6226
+ text += `- **Class:** \`${implClass}\`\n`;
6227
+ text += `- **di.xml:** \`${implFile}\`\n\n`;
6228
+
6229
+ // 3. Read the implementation method body
6230
+ const implPhpFile = findClassFile(root, implClass);
6231
+ if (implPhpFile) {
6232
+ const relImpl = implPhpFile.replace(root + '/', '');
6233
+ const body = readFullMethodBody(implPhpFile, matchedRoute.serviceMethod);
6234
+ if (body) {
6235
+ const implContent = readFileSync(implPhpFile, 'utf-8');
6236
+ const lines = implContent.split('\n');
6237
+ let mLine = 0;
6238
+ for (let i = 0; i < lines.length; i++) {
6239
+ if (lines[i].includes(`function ${matchedRoute.serviceMethod}(`)) { mLine = i + 1; break; }
6240
+ }
6241
+ text += `## ${matchedRoute.serviceMethod}() — \`${relImpl}\` (line ${mLine})\n\n`;
6242
+ text += '```php\n' + body + '\n```\n\n';
6243
+
6244
+ // 4. Check for collectTotals / key patterns
6245
+ const hasCollectTotals = body.includes('collectTotals');
6246
+ const hasEventDispatch = body.includes('dispatch') || body.includes('eventManager');
6247
+ text += `## Quick checks\n\n`;
6248
+ text += `- collectTotals() called: **${hasCollectTotals ? 'YES' : 'NO'}**\n`;
6249
+ text += `- Event dispatched: **${hasEventDispatch ? 'YES' : 'NO'}**\n`;
6250
+
6251
+ // 5. Extract constructor for DI understanding
6252
+ const ctorBody = readFullMethodBody(implPhpFile, '__construct');
6253
+ if (ctorBody) {
6254
+ text += `\n## Constructor\n\n`;
6255
+ text += '```php\n' + ctorBody + '\n```\n';
6256
+ }
6257
+ }
6258
+ }
6259
+
6260
+ // 6. Check di.xml for type arguments (e.g., allowed payment methods)
6261
+ for (const { content: diContent, relPath } of diFiles) {
6262
+ const implShort = implClass.split('\\').pop();
6263
+ if (!diContent.includes(implShort)) continue;
6264
+ const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
6265
+ let tm;
6266
+ while ((tm = typeBlockRegex.exec(diContent)) !== null) {
6267
+ if (tm[1].includes(implShort)) {
6268
+ text += `\n## DI Arguments — \`${relPath}\`\n\n`;
6269
+ text += '```xml\n' + tm[0] + '\n```\n';
6270
+ }
6271
+ }
6272
+ }
6273
+ }
6274
+
6275
+ // 7. Check for plugins on the interface
6276
+ for (const { content: diContent, relPath } of diFiles) {
6277
+ if (!diContent.includes(shortIface)) continue;
6278
+ const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
6279
+ let tm;
6280
+ while ((tm = typeBlockRegex.exec(diContent)) !== null) {
6281
+ if (tm[1].includes(shortIface) && tm[2].includes('<plugin')) {
6282
+ text += `\n## Plugins on \`${tm[1]}\` — \`${relPath}\`\n\n`;
6283
+ text += '```xml\n' + tm[0] + '\n```\n';
6284
+ }
6285
+ }
6286
+ }
6287
+
6288
+ return { content: [{ type: 'text', text }] };
6289
+ }
6290
+
6109
6291
  case 'magento_read': {
6110
6292
  const root = config.magentoRoot;
6111
6293
  if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
@@ -6114,11 +6296,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
6114
6296
  try { content = readFileSync(filePath, 'utf-8'); } catch (err) {
6115
6297
  return { content: [{ type: 'text', text: `File not found: ${args.path}` }], isError: true };
6116
6298
  }
6299
+
6300
+ // Method extraction mode: return only the specified method
6301
+ if (args.methodName) {
6302
+ const body = readFullMethodBody(filePath, args.methodName);
6303
+ if (!body) {
6304
+ return { content: [{ type: 'text', text: `## ${args.path}\n\nMethod \`${args.methodName}\` not found in file.` }] };
6305
+ }
6306
+ // Find line number of the method
6307
+ const lines = content.split('\n');
6308
+ let methodLine = 0;
6309
+ for (let i = 0; i < lines.length; i++) {
6310
+ if (lines[i].includes(`function ${args.methodName}(`)) { methodLine = i + 1; break; }
6311
+ }
6312
+ const bodyLines = body.split('\n');
6313
+ const numbered = bodyLines.map((line, i) => `${methodLine + i}\t${line}`).join('\n');
6314
+ return { content: [{ type: 'text', text: `## ${args.path}::${args.methodName}() (line ${methodLine})\n\n${numbered}` }] };
6315
+ }
6316
+
6117
6317
  const allLines = content.split('\n');
6118
6318
  const start = Math.max((args.startLine || 1) - 1, 0);
6119
6319
  const end = args.endLine ? Math.min(args.endLine, allLines.length) : allLines.length;
6120
6320
  const sliced = allLines.slice(start, end);
6121
- // Format with line numbers
6122
6321
  const numbered = sliced.map((line, i) => `${start + i + 1}\t${line}`).join('\n');
6123
6322
  let text = `## ${args.path}`;
6124
6323
  if (args.startLine || args.endLine) text += ` (lines ${start + 1}-${end})`;