magector 2.10.0 → 2.11.1
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.
- package/package.json +5 -5
- package/src/mcp-server.js +209 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.1",
|
|
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.
|
|
37
|
-
"@magector/cli-linux-x64": "2.
|
|
38
|
-
"@magector/cli-linux-arm64": "2.
|
|
39
|
-
"@magector/cli-win32-x64": "2.
|
|
36
|
+
"@magector/cli-darwin-arm64": "2.11.1",
|
|
37
|
+
"@magector/cli-linux-x64": "2.11.1",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.11.1",
|
|
39
|
+
"@magector/cli-win32-x64": "2.11.1"
|
|
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
|
|
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 {
|
|
@@ -4552,8 +4578,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4552
4578
|
if (pluginFile) {
|
|
4553
4579
|
reg.methods = extractPluginMethods(pluginFile);
|
|
4554
4580
|
reg.resolvedFile = pluginFile.replace(fpRoot2 + '/', '');
|
|
4555
|
-
// Read
|
|
4556
|
-
|
|
4581
|
+
// Read method bodies — only for targetMethod if specified (reduces token bloat)
|
|
4582
|
+
const methodsToRead = args.targetMethod
|
|
4583
|
+
? reg.methods.filter(m => m.targetMethod === args.targetMethod)
|
|
4584
|
+
: reg.methods;
|
|
4585
|
+
for (const m of methodsToRead) {
|
|
4557
4586
|
const body = readFullMethodBody(pluginFile, m.name);
|
|
4558
4587
|
if (body) m.body = body;
|
|
4559
4588
|
}
|
|
@@ -6027,6 +6056,17 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6027
6056
|
text = `File not found: ${a.path}`;
|
|
6028
6057
|
break;
|
|
6029
6058
|
}
|
|
6059
|
+
if (a.methodName) {
|
|
6060
|
+
const body = readFullMethodBody(filePath, a.methodName);
|
|
6061
|
+
if (!body) { text = `Method ${a.methodName} not found in ${a.path}`; break; }
|
|
6062
|
+
const fLines = fileContent.split('\n');
|
|
6063
|
+
let mLine = 0;
|
|
6064
|
+
for (let i = 0; i < fLines.length; i++) {
|
|
6065
|
+
if (fLines[i].includes(`function ${a.methodName}(`)) { mLine = i + 1; break; }
|
|
6066
|
+
}
|
|
6067
|
+
text = body.split('\n').map((line, i) => `${mLine + i}\t${line}`).join('\n');
|
|
6068
|
+
break;
|
|
6069
|
+
}
|
|
6030
6070
|
const allLines = fileContent.split('\n');
|
|
6031
6071
|
const s = Math.max((a.startLine || 1) - 1, 0);
|
|
6032
6072
|
const e = a.endLine ? Math.min(a.endLine, allLines.length) : allLines.length;
|
|
@@ -6039,7 +6079,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6039
6079
|
const include = a.include || '*.php';
|
|
6040
6080
|
const maxRes = Math.min(a.maxResults || 30, 100);
|
|
6041
6081
|
const batchCtx = a.context !== undefined ? a.context : 2;
|
|
6042
|
-
const gArgs = ['-rn'];
|
|
6082
|
+
const gArgs = ['-rn', '-E'];
|
|
6043
6083
|
if (a.ignoreCase) gArgs.push('-i');
|
|
6044
6084
|
if (batchCtx > 0) gArgs.push('-C', String(batchCtx));
|
|
6045
6085
|
for (const pat of include.split(',').map(p => p.trim())) gArgs.push('--include=' + pat);
|
|
@@ -6076,7 +6116,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6076
6116
|
const include = args.include || '*.php';
|
|
6077
6117
|
const maxResults = Math.min(args.maxResults || 50, 200);
|
|
6078
6118
|
const ctxLines = args.context !== undefined ? args.context : 2;
|
|
6079
|
-
const grepArgs = ['-rn'];
|
|
6119
|
+
const grepArgs = ['-rn', '-E'];
|
|
6080
6120
|
if (args.ignoreCase) grepArgs.push('-i');
|
|
6081
6121
|
if (ctxLines > 0) grepArgs.push('-C', String(ctxLines));
|
|
6082
6122
|
// Support multiple include patterns (e.g., "*.{php,xml}")
|
|
@@ -6106,6 +6146,151 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6106
6146
|
return { content: [{ type: 'text', text }] };
|
|
6107
6147
|
}
|
|
6108
6148
|
|
|
6149
|
+
case 'magento_trace_api': {
|
|
6150
|
+
const root = config.magentoRoot;
|
|
6151
|
+
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
6152
|
+
let text = '';
|
|
6153
|
+
|
|
6154
|
+
// 1. Find the endpoint in webapi.xml files
|
|
6155
|
+
const webapiFiles = await glob('**/etc/webapi.xml', { cwd: root, absolute: true, nodir: true });
|
|
6156
|
+
let matchedRoute = null;
|
|
6157
|
+
const searchUrl = args.url || '';
|
|
6158
|
+
const searchInterface = args.interfaceName || '';
|
|
6159
|
+
const searchMethod = args.method || '';
|
|
6160
|
+
|
|
6161
|
+
for (const wf of webapiFiles) {
|
|
6162
|
+
let wContent;
|
|
6163
|
+
try { wContent = readFileSync(wf, 'utf-8'); } catch { continue; }
|
|
6164
|
+
const relPath = wf.replace(root + '/', '');
|
|
6165
|
+
|
|
6166
|
+
const routeRegex = /<route\s+url="([^"]+)"\s+method="([^"]+)"[^>]*>([\s\S]*?)<\/route>/g;
|
|
6167
|
+
let rm;
|
|
6168
|
+
while ((rm = routeRegex.exec(wContent)) !== null) {
|
|
6169
|
+
const routeUrl = rm[1];
|
|
6170
|
+
const routeMethod = rm[2];
|
|
6171
|
+
const routeBody = rm[3];
|
|
6172
|
+
|
|
6173
|
+
const urlMatch = searchUrl ? routeUrl.includes(searchUrl) || searchUrl.includes(routeUrl) : false;
|
|
6174
|
+
const ifaceMatch = searchInterface ? routeBody.includes(searchInterface) : false;
|
|
6175
|
+
const methodMatch = searchMethod ? routeMethod === searchMethod : true;
|
|
6176
|
+
|
|
6177
|
+
if ((urlMatch || ifaceMatch) && methodMatch) {
|
|
6178
|
+
const serviceMatch = routeBody.match(/class="([^"]+)"\s+method="([^"]+)"/);
|
|
6179
|
+
if (serviceMatch) {
|
|
6180
|
+
matchedRoute = {
|
|
6181
|
+
url: routeUrl,
|
|
6182
|
+
httpMethod: routeMethod,
|
|
6183
|
+
serviceClass: serviceMatch[1],
|
|
6184
|
+
serviceMethod: serviceMatch[2],
|
|
6185
|
+
file: relPath
|
|
6186
|
+
};
|
|
6187
|
+
// Extract resource
|
|
6188
|
+
const resMatch = routeBody.match(/resource\s+ref="([^"]+)"/);
|
|
6189
|
+
if (resMatch) matchedRoute.acl = resMatch[1];
|
|
6190
|
+
break;
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
if (matchedRoute) break;
|
|
6195
|
+
}
|
|
6196
|
+
|
|
6197
|
+
if (!matchedRoute) {
|
|
6198
|
+
return { content: [{ type: 'text', text: `No API endpoint found matching url="${searchUrl}" interface="${searchInterface}"` }] };
|
|
6199
|
+
}
|
|
6200
|
+
|
|
6201
|
+
text += `## API Endpoint\n\n`;
|
|
6202
|
+
text += `- **URL:** \`${matchedRoute.httpMethod} ${matchedRoute.url}\`\n`;
|
|
6203
|
+
text += `- **Interface:** \`${matchedRoute.serviceClass}::${matchedRoute.serviceMethod}()\`\n`;
|
|
6204
|
+
text += `- **ACL:** \`${matchedRoute.acl || 'none'}\`\n`;
|
|
6205
|
+
text += `- **webapi.xml:** \`${matchedRoute.file}\`\n\n`;
|
|
6206
|
+
|
|
6207
|
+
// 2. Find DI preference (implementation)
|
|
6208
|
+
const diFiles = await getDiXmlFiles(root);
|
|
6209
|
+
const shortIface = matchedRoute.serviceClass.split('\\').pop();
|
|
6210
|
+
let implClass = null;
|
|
6211
|
+
let implFile = null;
|
|
6212
|
+
|
|
6213
|
+
for (const { content: diContent, relPath } of diFiles) {
|
|
6214
|
+
if (!diContent.includes(shortIface)) continue;
|
|
6215
|
+
const prefRegex = /<preference\s+for="([^"]+)"\s+type="([^"]+)"\s*\/?>/g;
|
|
6216
|
+
let pm;
|
|
6217
|
+
while ((pm = prefRegex.exec(diContent)) !== null) {
|
|
6218
|
+
if (pm[1].includes(shortIface)) {
|
|
6219
|
+
implClass = pm[2];
|
|
6220
|
+
implFile = relPath;
|
|
6221
|
+
break;
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
if (implClass) break;
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
if (implClass) {
|
|
6228
|
+
text += `## Implementation\n\n`;
|
|
6229
|
+
text += `- **Class:** \`${implClass}\`\n`;
|
|
6230
|
+
text += `- **di.xml:** \`${implFile}\`\n\n`;
|
|
6231
|
+
|
|
6232
|
+
// 3. Read the implementation method body
|
|
6233
|
+
const implPhpFile = findClassFile(root, implClass);
|
|
6234
|
+
if (implPhpFile) {
|
|
6235
|
+
const relImpl = implPhpFile.replace(root + '/', '');
|
|
6236
|
+
const body = readFullMethodBody(implPhpFile, matchedRoute.serviceMethod);
|
|
6237
|
+
if (body) {
|
|
6238
|
+
const implContent = readFileSync(implPhpFile, 'utf-8');
|
|
6239
|
+
const lines = implContent.split('\n');
|
|
6240
|
+
let mLine = 0;
|
|
6241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6242
|
+
if (lines[i].includes(`function ${matchedRoute.serviceMethod}(`)) { mLine = i + 1; break; }
|
|
6243
|
+
}
|
|
6244
|
+
text += `## ${matchedRoute.serviceMethod}() — \`${relImpl}\` (line ${mLine})\n\n`;
|
|
6245
|
+
text += '```php\n' + body + '\n```\n\n';
|
|
6246
|
+
|
|
6247
|
+
// 4. Check for collectTotals / key patterns
|
|
6248
|
+
const hasCollectTotals = body.includes('collectTotals');
|
|
6249
|
+
const hasEventDispatch = body.includes('dispatch') || body.includes('eventManager');
|
|
6250
|
+
text += `## Quick checks\n\n`;
|
|
6251
|
+
text += `- collectTotals() called: **${hasCollectTotals ? 'YES' : 'NO'}**\n`;
|
|
6252
|
+
text += `- Event dispatched: **${hasEventDispatch ? 'YES' : 'NO'}**\n`;
|
|
6253
|
+
|
|
6254
|
+
// 5. Extract constructor for DI understanding
|
|
6255
|
+
const ctorBody = readFullMethodBody(implPhpFile, '__construct');
|
|
6256
|
+
if (ctorBody) {
|
|
6257
|
+
text += `\n## Constructor\n\n`;
|
|
6258
|
+
text += '```php\n' + ctorBody + '\n```\n';
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6262
|
+
|
|
6263
|
+
// 6. Check di.xml for type arguments (e.g., allowed payment methods)
|
|
6264
|
+
for (const { content: diContent, relPath } of diFiles) {
|
|
6265
|
+
const implShort = implClass.split('\\').pop();
|
|
6266
|
+
if (!diContent.includes(implShort)) continue;
|
|
6267
|
+
const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
|
|
6268
|
+
let tm;
|
|
6269
|
+
while ((tm = typeBlockRegex.exec(diContent)) !== null) {
|
|
6270
|
+
if (tm[1].includes(implShort)) {
|
|
6271
|
+
text += `\n## DI Arguments — \`${relPath}\`\n\n`;
|
|
6272
|
+
text += '```xml\n' + tm[0] + '\n```\n';
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6276
|
+
}
|
|
6277
|
+
|
|
6278
|
+
// 7. Check for plugins on the interface
|
|
6279
|
+
for (const { content: diContent, relPath } of diFiles) {
|
|
6280
|
+
if (!diContent.includes(shortIface)) continue;
|
|
6281
|
+
const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
|
|
6282
|
+
let tm;
|
|
6283
|
+
while ((tm = typeBlockRegex.exec(diContent)) !== null) {
|
|
6284
|
+
if (tm[1].includes(shortIface) && tm[2].includes('<plugin')) {
|
|
6285
|
+
text += `\n## Plugins on \`${tm[1]}\` — \`${relPath}\`\n\n`;
|
|
6286
|
+
text += '```xml\n' + tm[0] + '\n```\n';
|
|
6287
|
+
}
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
|
|
6291
|
+
return { content: [{ type: 'text', text }] };
|
|
6292
|
+
}
|
|
6293
|
+
|
|
6109
6294
|
case 'magento_read': {
|
|
6110
6295
|
const root = config.magentoRoot;
|
|
6111
6296
|
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
@@ -6114,11 +6299,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6114
6299
|
try { content = readFileSync(filePath, 'utf-8'); } catch (err) {
|
|
6115
6300
|
return { content: [{ type: 'text', text: `File not found: ${args.path}` }], isError: true };
|
|
6116
6301
|
}
|
|
6302
|
+
|
|
6303
|
+
// Method extraction mode: return only the specified method
|
|
6304
|
+
if (args.methodName) {
|
|
6305
|
+
const body = readFullMethodBody(filePath, args.methodName);
|
|
6306
|
+
if (!body) {
|
|
6307
|
+
return { content: [{ type: 'text', text: `## ${args.path}\n\nMethod \`${args.methodName}\` not found in file.` }] };
|
|
6308
|
+
}
|
|
6309
|
+
// Find line number of the method
|
|
6310
|
+
const lines = content.split('\n');
|
|
6311
|
+
let methodLine = 0;
|
|
6312
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6313
|
+
if (lines[i].includes(`function ${args.methodName}(`)) { methodLine = i + 1; break; }
|
|
6314
|
+
}
|
|
6315
|
+
const bodyLines = body.split('\n');
|
|
6316
|
+
const numbered = bodyLines.map((line, i) => `${methodLine + i}\t${line}`).join('\n');
|
|
6317
|
+
return { content: [{ type: 'text', text: `## ${args.path}::${args.methodName}() (line ${methodLine})\n\n${numbered}` }] };
|
|
6318
|
+
}
|
|
6319
|
+
|
|
6117
6320
|
const allLines = content.split('\n');
|
|
6118
6321
|
const start = Math.max((args.startLine || 1) - 1, 0);
|
|
6119
6322
|
const end = args.endLine ? Math.min(args.endLine, allLines.length) : allLines.length;
|
|
6120
6323
|
const sliced = allLines.slice(start, end);
|
|
6121
|
-
// Format with line numbers
|
|
6122
6324
|
const numbered = sliced.map((line, i) => `${start + i + 1}\t${line}`).join('\n');
|
|
6123
6325
|
let text = `## ${args.path}`;
|
|
6124
6326
|
if (args.startLine || args.endLine) text += ` (lines ${start + 1}-${end})`;
|