magector 2.9.1 → 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.
- package/package.json +5 -5
- package/src/mcp-server.js +280 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
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.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
|
@@ -4143,13 +4143,61 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
4143
4143
|
},
|
|
4144
4144
|
context: {
|
|
4145
4145
|
type: 'number',
|
|
4146
|
-
description: 'Lines of context around each match (default:
|
|
4147
|
-
default:
|
|
4146
|
+
description: 'Lines of context around each match (default: 2). Like grep -C.',
|
|
4147
|
+
default: 2
|
|
4148
4148
|
}
|
|
4149
4149
|
},
|
|
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
|
+
},
|
|
4175
|
+
{
|
|
4176
|
+
name: 'magento_read',
|
|
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.',
|
|
4178
|
+
inputSchema: {
|
|
4179
|
+
type: 'object',
|
|
4180
|
+
properties: {
|
|
4181
|
+
path: {
|
|
4182
|
+
type: 'string',
|
|
4183
|
+
description: 'File path relative to MAGENTO_ROOT. Example: "vendor/acme/module-sales/Model/OrderService.php"'
|
|
4184
|
+
},
|
|
4185
|
+
startLine: {
|
|
4186
|
+
type: 'number',
|
|
4187
|
+
description: 'Start reading from this line number (1-based). Default: 1 (beginning of file).'
|
|
4188
|
+
},
|
|
4189
|
+
endLine: {
|
|
4190
|
+
type: 'number',
|
|
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"'
|
|
4196
|
+
}
|
|
4197
|
+
},
|
|
4198
|
+
required: ['path']
|
|
4199
|
+
}
|
|
4200
|
+
},
|
|
4153
4201
|
]
|
|
4154
4202
|
}));
|
|
4155
4203
|
|
|
@@ -4168,7 +4216,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4168
4216
|
// These tools have filesystem/di.xml fallbacks — work without serve process
|
|
4169
4217
|
'magento_find_class', 'magento_find_method', 'magento_find_plugin',
|
|
4170
4218
|
'magento_find_observer', 'magento_find_di_wiring', 'magento_module_structure',
|
|
4171
|
-
'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep'];
|
|
4219
|
+
'magento_batch', 'magento_find_config', 'magento_find_callers', 'magento_grep', 'magento_read', 'magento_trace_api'];
|
|
4172
4220
|
if (warmupInProgress && !indexFreeTools.includes(name)) {
|
|
4173
4221
|
logToFile('REQ', `${name} → blocked (warmup: loading index)`);
|
|
4174
4222
|
return {
|
|
@@ -4969,7 +5017,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4969
5017
|
structure: structureOutput.categories
|
|
4970
5018
|
});
|
|
4971
5019
|
|
|
4972
|
-
|
|
5020
|
+
// Include README.md if it exists in the module directory
|
|
5021
|
+
let readmeText = '';
|
|
5022
|
+
if (results.length > 0) {
|
|
5023
|
+
// Find module root from first result path
|
|
5024
|
+
const firstPath = results[0].path || '';
|
|
5025
|
+
const moduleRoot = firstPath.split('/').slice(0, 3).join('/');
|
|
5026
|
+
if (moduleRoot) {
|
|
5027
|
+
const readmePath = path.join(config.magentoRoot, moduleRoot, 'README.md');
|
|
5028
|
+
try {
|
|
5029
|
+
const readme = readFileSync(readmePath, 'utf-8');
|
|
5030
|
+
readmeText = '\n\n## README.md\n\n' + readme.slice(0, 2000) + (readme.length > 2000 ? '\n...(truncated)' : '');
|
|
5031
|
+
} catch { /* no README */ }
|
|
5032
|
+
}
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
return { content: [{ type: 'text', text: jsonOutput + readmeText }] };
|
|
4973
5036
|
}
|
|
4974
5037
|
|
|
4975
5038
|
case 'magento_analyze_diff': {
|
|
@@ -5983,13 +6046,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5983
6046
|
}
|
|
5984
6047
|
break;
|
|
5985
6048
|
}
|
|
6049
|
+
case 'magento_read': {
|
|
6050
|
+
const filePath = path.join(config.magentoRoot, a.path);
|
|
6051
|
+
let fileContent;
|
|
6052
|
+
try { fileContent = readFileSync(filePath, 'utf-8'); } catch {
|
|
6053
|
+
text = `File not found: ${a.path}`;
|
|
6054
|
+
break;
|
|
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
|
+
}
|
|
6067
|
+
const allLines = fileContent.split('\n');
|
|
6068
|
+
const s = Math.max((a.startLine || 1) - 1, 0);
|
|
6069
|
+
const e = a.endLine ? Math.min(a.endLine, allLines.length) : allLines.length;
|
|
6070
|
+
const sl = allLines.slice(s, e);
|
|
6071
|
+
text = sl.map((line, i) => `${s + i + 1}\t${line}`).join('\n');
|
|
6072
|
+
break;
|
|
6073
|
+
}
|
|
5986
6074
|
case 'magento_grep': {
|
|
5987
6075
|
const searchPath = a.path || '.';
|
|
5988
6076
|
const include = a.include || '*.php';
|
|
5989
6077
|
const maxRes = Math.min(a.maxResults || 30, 100);
|
|
5990
|
-
const
|
|
6078
|
+
const batchCtx = a.context !== undefined ? a.context : 2;
|
|
6079
|
+
const gArgs = ['-rn', '-E'];
|
|
5991
6080
|
if (a.ignoreCase) gArgs.push('-i');
|
|
5992
|
-
if (
|
|
6081
|
+
if (batchCtx > 0) gArgs.push('-C', String(batchCtx));
|
|
5993
6082
|
for (const pat of include.split(',').map(p => p.trim())) gArgs.push('--include=' + pat);
|
|
5994
6083
|
gArgs.push('--', a.pattern, searchPath);
|
|
5995
6084
|
let out;
|
|
@@ -6023,9 +6112,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6023
6112
|
const searchPath = args.path || '.';
|
|
6024
6113
|
const include = args.include || '*.php';
|
|
6025
6114
|
const maxResults = Math.min(args.maxResults || 50, 200);
|
|
6026
|
-
const
|
|
6115
|
+
const ctxLines = args.context !== undefined ? args.context : 2;
|
|
6116
|
+
const grepArgs = ['-rn', '-E'];
|
|
6027
6117
|
if (args.ignoreCase) grepArgs.push('-i');
|
|
6028
|
-
if (
|
|
6118
|
+
if (ctxLines > 0) grepArgs.push('-C', String(ctxLines));
|
|
6029
6119
|
// Support multiple include patterns (e.g., "*.{php,xml}")
|
|
6030
6120
|
for (const pat of include.split(',').map(p => p.trim())) {
|
|
6031
6121
|
grepArgs.push('--include=' + pat);
|
|
@@ -6053,6 +6143,188 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6053
6143
|
return { content: [{ type: 'text', text }] };
|
|
6054
6144
|
}
|
|
6055
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
|
+
|
|
6291
|
+
case 'magento_read': {
|
|
6292
|
+
const root = config.magentoRoot;
|
|
6293
|
+
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
6294
|
+
const filePath = path.join(root, args.path);
|
|
6295
|
+
let content;
|
|
6296
|
+
try { content = readFileSync(filePath, 'utf-8'); } catch (err) {
|
|
6297
|
+
return { content: [{ type: 'text', text: `File not found: ${args.path}` }], isError: true };
|
|
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
|
+
|
|
6317
|
+
const allLines = content.split('\n');
|
|
6318
|
+
const start = Math.max((args.startLine || 1) - 1, 0);
|
|
6319
|
+
const end = args.endLine ? Math.min(args.endLine, allLines.length) : allLines.length;
|
|
6320
|
+
const sliced = allLines.slice(start, end);
|
|
6321
|
+
const numbered = sliced.map((line, i) => `${start + i + 1}\t${line}`).join('\n');
|
|
6322
|
+
let text = `## ${args.path}`;
|
|
6323
|
+
if (args.startLine || args.endLine) text += ` (lines ${start + 1}-${end})`;
|
|
6324
|
+
text += `\n\n${numbered}`;
|
|
6325
|
+
return { content: [{ type: 'text', text }] };
|
|
6326
|
+
}
|
|
6327
|
+
|
|
6056
6328
|
default:
|
|
6057
6329
|
return {
|
|
6058
6330
|
content: [{
|