phpstan-mcp-mx 0.8.0 → 0.10.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 (43) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +3 -3
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/phpstan/error.d.ts +8 -0
  6. package/dist/phpstan/error.js +9 -0
  7. package/dist/phpstan/error.js.map +1 -0
  8. package/dist/phpstan/output.d.ts +57 -0
  9. package/dist/phpstan/output.js +76 -0
  10. package/dist/phpstan/output.js.map +1 -0
  11. package/dist/phpstan/process.d.ts +45 -0
  12. package/dist/phpstan/process.js +148 -0
  13. package/dist/phpstan/process.js.map +1 -0
  14. package/dist/phpstan/queue.d.ts +21 -0
  15. package/dist/phpstan/queue.js +76 -0
  16. package/dist/phpstan/queue.js.map +1 -0
  17. package/dist/phpstan.d.ts +7 -65
  18. package/dist/phpstan.js +8 -289
  19. package/dist/phpstan.js.map +1 -1
  20. package/dist/server.js +4 -0
  21. package/dist/server.js.map +1 -1
  22. package/dist/tools/analysis.d.ts +8 -0
  23. package/dist/tools/analysis.js +131 -0
  24. package/dist/tools/analysis.js.map +1 -0
  25. package/dist/tools/inspect.d.ts +14 -0
  26. package/dist/tools/inspect.js +218 -0
  27. package/dist/tools/inspect.js.map +1 -0
  28. package/dist/tools/prompts.d.ts +3 -0
  29. package/dist/tools/prompts.js +45 -0
  30. package/dist/tools/prompts.js.map +1 -0
  31. package/dist/tools/shared.d.ts +160 -0
  32. package/dist/tools/shared.js +219 -0
  33. package/dist/tools/shared.js.map +1 -0
  34. package/dist/tools/writes.d.ts +17 -0
  35. package/dist/tools/writes.js +131 -0
  36. package/dist/tools/writes.js.map +1 -0
  37. package/dist/tools.d.ts +0 -11
  38. package/dist/tools.js +4 -671
  39. package/dist/tools.js.map +1 -1
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/dist/version.js.map +1 -1
  43. package/package.json +14 -4
package/dist/tools.js CHANGED
@@ -1,103 +1,7 @@
1
- import { z } from 'zod';
2
- import { BaselineError, describeIncludedBaseline, renderBaseline, summariseBaseline } from './baseline.js';
3
- import { DEFAULT_BASE, GitError, changedPhpFiles } from './changed.js';
4
- import { PhpstanError, analyse, clearResultCache, diagnose, generateBaseline, loadDefaultMemoryLimit, loadMaxParallel, } from './phpstan.js';
5
- import { DEFAULT_LIMIT, MAX_LIMIT, buildReport, displayPath, renderReport } from './report.js';
6
- import { ProjectError, discoverProjects, resolveNewWithin, resolveProject, resolveWithin } from './project.js';
7
- import { TmpDirError, loadTmpDirMode } from './tmpdir.js';
8
- import { ROOTS_ENV_VAR } from './roots.js';
9
- import { VERSION } from './version.js';
10
- /** Env-variabele die schrijvende tools vrijgeeft. */
11
- export const WRITES_ENV_VAR = 'PHPSTAN_MCP_ALLOW_WRITES';
12
- /**
13
- * Of schrijvende tools geregistreerd mogen worden. Alleen "1" en "true" tellen; al
14
- * het andere staat uit. Dat faalt de veilige kant op: een typfout laat schrijven
15
- * uit, en de opstartregel zegt wat er geldt.
16
- *
17
- * Niet registreren en niet weigeren-bij-de-aanroep: een tool die bestaat maar altijd
18
- * nee zegt, kost het model een ronde en context om dat te ontdekken.
19
- */
20
- export function writesAllowed(env = process.env) {
21
- const configured = env[WRITES_ENV_VAR]?.trim().toLowerCase();
22
- return configured === '1' || configured === 'true';
23
- }
24
- /**
25
- * Nullable veld als `anyOf` in plaats van `type: [..., "null"]`. Zod's `.nullable()`
26
- * levert de array-vorm op; die is geldig JSON Schema, maar meerdere MCP-clients
27
- * lezen `type` als één string en laten de constraint dan vallen of weigeren de tool.
28
- *
29
- * Let op de `z.literal(null)`: zod vouwt zowel `.nullable()` als een union met
30
- * `z.null()` terug naar diezelfde array-vorm. Alleen de literal levert echt `anyOf`
31
- * op (met een overbodige maar onschadelijke `const: null` erbij).
32
- */
33
- function nullable(schema) {
34
- return z.union([schema, z.literal(null)]);
35
- }
36
- const projectArgument = z
37
- .string()
38
- .describe('Absoluut pad naar de projectroot, zoals list_projects het teruggeeft');
39
- const analysisShape = {
40
- project: z.string(),
41
- totals: z.object({ fileErrors: z.number(), generalErrors: z.number() }),
42
- generalErrors: z
43
- .array(z.string())
44
- .describe('Fouten die niet aan een bestand hangen, zoals een crash of configuratiefout'),
45
- warnings: z
46
- .array(z.string())
47
- .describe('Wat PHPStan naast het resultaat meldde. Staat hier "Result is incomplete", dan ' +
48
- 'dekt de analyse niet het hele project en is een schone uitslag geen bewijs'),
49
- notices: z
50
- .array(z.string())
51
- .describe('Meldingen van deze server zelf, zoals een baseline die meedoet — dan rapporteert ' +
52
- 'PHPStan alleen wat er nieuw bij komt'),
53
- omitted: z
54
- .object({ warnings: z.number(), generalErrors: z.number() })
55
- .describe('Hoeveel meldingen weggelaten zijn omdat het er te veel waren'),
56
- summary: z
57
- .object({
58
- filesWithFindings: z.number(),
59
- byFile: z.array(z.object({ file: z.string(), count: z.number() })),
60
- byIdentifier: z.array(z.object({ identifier: z.string(), count: z.number() })),
61
- })
62
- .describe('Tellingen over álle bevindingen, ook die buiten deze pagina vallen'),
63
- findings: z.array(z.object({
64
- file: z.string().describe('Pad relatief aan de projectroot'),
65
- line: nullable(z.number()),
66
- message: z.string(),
67
- identifier: nullable(z.string()),
68
- tip: nullable(z.string()),
69
- })),
70
- page: z
71
- .object({
72
- offset: z.number(),
73
- limit: z.number(),
74
- returned: z.number(),
75
- total: z.number(),
76
- hasMore: z.boolean(),
77
- })
78
- .describe('Welk deel van de bevindingen dit antwoord bevat'),
79
- filter: nullable(z.array(z.string())).describe('Op welke identifiers deze pagina beperkt is, of null als er niet gefilterd is'),
80
- };
81
- const configArgument = z
82
- .string()
83
- .optional()
84
- .describe('Ander configbestand dan PHPStan zelf zou pakken, bijv. "phpstan-strict.neon". ' +
85
- 'Moet binnen het project liggen');
86
- const windowShape = {
87
- identifiers: z
88
- .array(z.string())
89
- .optional()
90
- .describe('Beperk de bevindingen tot deze identifiers, bijv. ["argument.type"]. ' +
91
- '"missingType" matcht de hele groep. De samenvatting blijft over alles tellen'),
92
- offset: z.number().int().min(0).optional().describe('Sla de eerste N bevindingen over'),
93
- limit: z
94
- .number()
95
- .int()
96
- .min(1)
97
- .max(MAX_LIMIT)
98
- .optional()
99
- .describe(`Hoeveel bevindingen dit antwoord bevat (standaard ${String(DEFAULT_LIMIT)})`),
100
- };
1
+ import { registerAnalyseChanged, registerAnalyseFile, registerAnalyseProject, } from './tools/analysis.js';
2
+ import { registerBaselineSummary, registerDiagnose, registerListProjects, registerServerConfig, } from './tools/inspect.js';
3
+ import { registerPrompts } from './tools/prompts.js';
4
+ import { registerClearResultCache, registerGenerateBaseline, writesAllowed, } from './tools/writes.js';
101
5
  /** Hangt alle tools aan de server. */
102
6
  export function registerTools(server, roots) {
103
7
  registerListProjects(server, roots);
@@ -112,575 +16,4 @@ export function registerTools(server, roots) {
112
16
  registerGenerateBaseline(server, roots);
113
17
  registerPrompts(server);
114
18
  }
115
- function registerBaselineSummary(server, roots) {
116
- server.registerTool('baseline_summary', {
117
- title: 'Baseline samenvatten',
118
- description: 'Leest de phpstan-baseline*.neon van een project en telt wat er genegeerd wordt, ' +
119
- 'per identifier en per bestand. Draait geen analyse. Let op: wat hier in staat is ' +
120
- 'precies wat analyse_project NIET meldt — een project dat zijn baseline includet, ' +
121
- 'rapporteert alleen de fouten die er nieuw bij komen.',
122
- inputSchema: { project: projectArgument },
123
- outputSchema: {
124
- project: z.string(),
125
- files: z.array(z.object({ file: z.string(), entries: z.number(), errors: z.number() })),
126
- totals: z.object({ entries: z.number(), errors: z.number() }),
127
- byIdentifier: z.array(z.object({ identifier: z.string(), count: z.number() })),
128
- byPath: z.array(z.object({ path: z.string(), count: z.number() })),
129
- },
130
- annotations: readOnly(),
131
- }, ({ project }) => {
132
- try {
133
- const summary = summariseBaseline(resolveProject(project, roots));
134
- return {
135
- content: [{ type: 'text', text: renderBaseline(summary) }],
136
- structuredContent: { ...summary },
137
- };
138
- }
139
- catch (error) {
140
- return toFailure(error);
141
- }
142
- });
143
- }
144
- function registerDiagnose(server, roots) {
145
- server.registerTool('diagnose_project', {
146
- title: 'PHPStan-opstelling opvragen',
147
- description: 'Vraagt PHPStan zelf om zijn opstelling in dit project: PHP- en PHPStan-versie, de ' +
148
- 'configs die Composer-pakketten meebrengen (zoals Larastan) en of de turbo-extensie ' +
149
- 'aanstaat. Draait geen analyse. Gebruik dit als een project zich anders gedraagt dan ' +
150
- 'een ander project, vóórdat je aan de code gaat twijfelen.',
151
- inputSchema: {
152
- project: projectArgument,
153
- timeoutSeconds: z.number().int().positive().max(1800).optional(),
154
- },
155
- outputSchema: {
156
- project: z.string(),
157
- output: z.string().describe('PHPStans eigen tekst, ongewijzigd'),
158
- },
159
- annotations: readOnly(),
160
- }, async (args, extra) => {
161
- try {
162
- const resolved = resolveProject(args.project, roots);
163
- const diagnosis = await diagnose(resolved, {
164
- ...(args.timeoutSeconds === undefined ? {} : { timeoutMs: args.timeoutSeconds * 1000 }),
165
- signal: extra.signal,
166
- });
167
- return {
168
- content: [{ type: 'text', text: diagnosis.output }],
169
- structuredContent: { ...diagnosis },
170
- };
171
- }
172
- catch (error) {
173
- return toFailure(error);
174
- }
175
- });
176
- }
177
- /**
178
- * Eén prompt met het werkpatroon. Er zijn zes tools waarvan drie op elkaar lijken;
179
- * zonder dit moet het model de juiste volgorde uit de losse beschrijvingen
180
- * reconstrueren, en dan is `analyse_project` op een groot project de eerste gok.
181
- */
182
- function registerServerConfig(server, roots) {
183
- server.registerTool('server_config', {
184
- title: 'Instellingen van deze server',
185
- description: 'Geeft de instellingen terug waarmee deze server draait: welke projectmappen binnen ' +
186
- 'bereik liggen, de standaard geheugenlimiet, hoeveel analyses er tegelijk mogen, waar ' +
187
- 'de tijdelijke bestanden heen gaan en of schrijvende tools aanstaan. Gebruik dit als ' +
188
- 'iets geweigerd wordt of trager gaat dan verwacht, vóórdat je aan het project twijfelt.',
189
- inputSchema: {},
190
- outputSchema: {
191
- version: z.string(),
192
- roots: z.array(z.object({
193
- raw: z.string().describe('Zoals opgegeven in de env-variabele'),
194
- path: nullable(z.string()).describe('Het geresolvede pad, of null als de root onbruikbaar is'),
195
- problem: nullable(z.string()),
196
- })),
197
- memoryLimit: nullable(z.string()).describe('Standaard --memory-limit, of null als die niet gezet is'),
198
- maxParallel: nullable(z.number()).describe('Hoeveel analyses tegelijk; null als de waarde geweigerd is'),
199
- tmpDir: nullable(z.string()).describe('"project" of "shared"'),
200
- writesAllowed: z.boolean().describe('Of generate_baseline geregistreerd is'),
201
- problems: z
202
- .array(z.string())
203
- .describe('Instellingen die geweigerd zijn; die velden staan dan op null'),
204
- },
205
- annotations: readOnly(),
206
- }, () => {
207
- // Elke waarde apart uitlezen: de loaders weigeren een onbruikbare waarde met
208
- // een uitleg, en juist die uitleg is hier het antwoord. Eén mislukte
209
- // instelling mag de rest niet onzichtbaar maken.
210
- const problems = [];
211
- const config = {
212
- version: VERSION,
213
- roots: roots.map((root) => ({ raw: root.raw, path: root.path, problem: root.problem })),
214
- memoryLimit: setting(() => loadDefaultMemoryLimit() ?? null, problems),
215
- maxParallel: setting(() => loadMaxParallel(), problems),
216
- tmpDir: setting(() => loadTmpDirMode(), problems),
217
- writesAllowed: writesAllowed(),
218
- problems,
219
- };
220
- return {
221
- content: [{ type: 'text', text: renderConfig(config) }],
222
- structuredContent: { ...config },
223
- };
224
- });
225
- }
226
- /**
227
- * Leest één instelling en vangt een geweigerde waarde op als melding in plaats van
228
- * als exception. Bewust geen doorgeefluik naar process.env: in diezelfde omgeving
229
- * zitten tokens en van alles, en die horen niet in een toolantwoord.
230
- */
231
- function setting(read, problems) {
232
- try {
233
- return read();
234
- }
235
- catch (error) {
236
- problems.push(error instanceof Error ? error.message : String(error));
237
- return null;
238
- }
239
- }
240
- function renderConfig(config) {
241
- const lines = [`phpstan-mcp ${config.version}`, '', 'Projectmappen:'];
242
- if (config.roots.length === 0) {
243
- lines.push(` (geen; zet ${ROOTS_ENV_VAR})`);
244
- }
245
- else {
246
- for (const root of config.roots) {
247
- lines.push(` ${root.path ?? root.raw}${root.problem === null ? '' : ` — ${root.problem}`}`);
248
- }
249
- }
250
- lines.push('', `Geheugenlimiet: ${config.memoryLimit ?? 'niet gezet (die van PHP zelf)'}`, `Tegelijk: ${config.maxParallel === null ? 'onbekend' : String(config.maxParallel)}`, `Tijdelijke map: ${config.tmpDir === null ? 'onbekend' : config.tmpDir}`, `Schrijvende tools: ${config.writesAllowed ? 'aan (generate_baseline bestaat)' : 'uit'}`);
251
- if (config.problems.length > 0) {
252
- lines.push('', 'Geweigerde instellingen:');
253
- for (const problem of config.problems)
254
- lines.push(`- ${problem}`);
255
- }
256
- return lines.join('\n');
257
- }
258
- function registerClearResultCache(server, roots) {
259
- server.registerTool('clear_result_cache', {
260
- title: 'Resultcache legen',
261
- description: 'Leegt PHPStans resultcache, zodat de volgende analyse alles opnieuw doorrekent. ' +
262
- 'Gebruik dit als een uitslag niet klopt met de code — bijvoorbeeld na een wijziging ' +
263
- 'aan een extensie of een geïncludeerde config. Let op: die cache ligt standaard niet ' +
264
- 'in het project maar in de tijdelijke map van het systeem, en wordt dan gedeeld met ' +
265
- 'elk ander project dat geen eigen tmpDir zet. Het antwoord noemt de map die geleegd is.',
266
- inputSchema: {
267
- project: projectArgument,
268
- config: configArgument,
269
- timeoutSeconds: z.number().int().positive().max(1800).optional(),
270
- },
271
- outputSchema: { project: z.string(), output: z.string() },
272
- annotations: clears(),
273
- }, async (args, extra) => {
274
- try {
275
- const resolved = resolveProject(args.project, roots);
276
- const cleared = await clearResultCache(resolved, {
277
- ...(args.config === undefined ? {} : { config: resolveWithin(resolved, args.config) }),
278
- ...(args.timeoutSeconds === undefined ? {} : { timeoutMs: args.timeoutSeconds * 1000 }),
279
- signal: extra.signal,
280
- });
281
- return {
282
- content: [{ type: 'text', text: cleared.output }],
283
- structuredContent: { ...cleared },
284
- };
285
- }
286
- catch (error) {
287
- return toFailure(error);
288
- }
289
- });
290
- }
291
- /** Standaardnaam, dezelfde die PHPStan zelf gebruikt. */
292
- const DEFAULT_BASELINE = 'phpstan-baseline.neon';
293
- function registerGenerateBaseline(server, roots) {
294
- server.registerTool('generate_baseline', {
295
- title: 'Baseline genereren',
296
- description: 'Zet alle huidige fouten in een baseline-bestand, zodat een volgende analyse alleen ' +
297
- 'meldt wat er nieuw bij komt. Bedoeld om een bestaand project onder PHPStan te ' +
298
- 'krijgen, niet om fouten weg te werken: wat hierin komt, wordt daarna niet meer ' +
299
- 'gemeld. Overschrijft een bestaande baseline. Het bestand moet nog wel in de config ' +
300
- 'geïncludeerd worden voordat het effect heeft.',
301
- inputSchema: {
302
- project: projectArgument,
303
- file: z
304
- .string()
305
- .optional()
306
- .describe(`Doelbestand binnen het project (standaard ${DEFAULT_BASELINE})`),
307
- paths: z.array(z.string()).optional().describe('Paden binnen het project'),
308
- level: z.string().optional().describe('Level-override, 0 t/m 10 of "max"'),
309
- config: configArgument,
310
- allowEmpty: z
311
- .boolean()
312
- .optional()
313
- .describe('Ook schrijven als er niets te negeren valt; anders weigert PHPStan'),
314
- memoryLimit: z.string().optional(),
315
- timeoutSeconds: z.number().int().positive().max(1800).optional(),
316
- },
317
- outputSchema: {
318
- project: z.string(),
319
- file: z.string(),
320
- errors: z.number().describe('Hoeveel fouten er in de baseline gezet zijn'),
321
- output: z.string(),
322
- },
323
- annotations: writes(),
324
- }, async (args, extra) => {
325
- try {
326
- const resolved = resolveProject(args.project, roots);
327
- const generated = await generateBaseline(resolved, {
328
- file: resolveNewWithin(resolved, args.file ?? DEFAULT_BASELINE),
329
- ...(args.paths === undefined
330
- ? {}
331
- : { paths: args.paths.map((path) => resolveWithin(resolved, path)) }),
332
- ...(args.allowEmpty === undefined ? {} : { allowEmpty: args.allowEmpty }),
333
- ...common(resolved, args),
334
- signal: extra.signal,
335
- });
336
- return {
337
- content: [{ type: 'text', text: renderGenerated(resolved, generated) }],
338
- structuredContent: { ...generated },
339
- };
340
- }
341
- catch (error) {
342
- return toFailure(error);
343
- }
344
- });
345
- }
346
- /** De uitkomst plus wat er nog moet gebeuren voordat de baseline iets doet. */
347
- function renderGenerated(project, generated) {
348
- const naam = displayPath(project.path, generated.file);
349
- return [
350
- `${String(generated.errors)} fout(en) in ${naam} gezet.`,
351
- '',
352
- `Deze fouten worden vanaf nu niet meer gemeld zodra ${naam} in de configuratie staat:`,
353
- '',
354
- 'includes:',
355
- ` - ${naam}`,
356
- ].join('\n');
357
- }
358
- function registerPrompts(server) {
359
- server.registerPrompt('phpstan_wijziging_nakijken', {
360
- title: 'Eigen wijziging nakijken met PHPStan',
361
- description: 'Het werkpatroon om je eigen wijziging na te kijken zonder het hele project door te rekenen.',
362
- argsSchema: {
363
- project: z
364
- .string()
365
- .optional()
366
- .describe('Absoluut pad naar de projectroot; laat leeg om eerst list_projects te doen'),
367
- },
368
- }, ({ project }) => ({
369
- messages: [
370
- {
371
- role: 'user',
372
- content: {
373
- type: 'text',
374
- text: [
375
- project === undefined || project === ''
376
- ? 'Roep eerst `list_projects` aan en kies het project waar ik in werk.'
377
- : `Werk in het project ${project}.`,
378
- '',
379
- 'Ga daarna zo te werk:',
380
- '1. `analyse_changed` met dat project — dat pakt alleen wat git gewijzigd heeft,',
381
- ' inclusief bestanden die nog niet toegevoegd zijn. Doe dit ook als het project',
382
- ' groot is; `analyse_project` zonder paden rekent alles door.',
383
- '2. Staat er "LET OP" of "Ter info" boven het rapport, lees dat eerst: een',
384
- ' onvolledige analyse of een baseline die meedoet maakt een schone uitslag',
385
- ' onbetrouwbaar.',
386
- '3. Repareer de bevindingen bij de bron. Verzwak geen types naar `mixed` en voeg',
387
- ' niets aan de baseline toe.',
388
- '4. Draai `analyse_changed` opnieuw tot het schoon is. Blijven er veel',
389
- ' bevindingen van één soort over, gebruik dan `identifiers` om er per soort',
390
- ' door te lopen.',
391
- '',
392
- 'Gedraagt het project zich onverklaarbaar, draai dan `diagnose_project` voordat',
393
- 'je aan de code twijfelt.',
394
- ].join('\n'),
395
- },
396
- },
397
- ],
398
- }));
399
- }
400
- function registerListProjects(server, roots) {
401
- server.registerTool('list_projects', {
402
- title: 'PHP-projecten opsommen',
403
- description: 'Somt de PHP-projecten op die deze server mag analyseren, met per project of ' +
404
- 'PHPStan geïnstalleerd is en welk configbestand er ligt. Gebruik het `path` uit ' +
405
- 'het resultaat als `project`-argument voor de andere tools.',
406
- inputSchema: {},
407
- outputSchema: {
408
- projects: z.array(z.object({
409
- path: z.string(),
410
- root: z.string(),
411
- hasPhpstan: z.boolean(),
412
- config: nullable(z.string()),
413
- hasGit: z.boolean().describe('Of analyse_changed hier bruikbaar is'),
414
- })),
415
- problems: z.array(z.object({ root: z.string(), problem: z.string() })),
416
- },
417
- annotations: readOnly(),
418
- }, () => {
419
- if (roots.length === 0) {
420
- return failure(`Er zijn geen projectmappen geconfigureerd. Zet ${ROOTS_ENV_VAR} in de ` +
421
- 'MCP-serverconfiguratie op de absolute paden die geanalyseerd mogen worden, ' +
422
- 'gescheiden door een dubbele punt.');
423
- }
424
- const projects = discoverProjects(roots);
425
- const problems = roots
426
- .filter((root) => root.problem !== null)
427
- .map((root) => ({ root: root.raw, problem: root.problem }));
428
- return {
429
- content: [{ type: 'text', text: renderProjects(projects, problems) }],
430
- structuredContent: { projects, problems },
431
- };
432
- });
433
- }
434
- function registerAnalyseProject(server, roots) {
435
- server.registerTool('analyse_project', {
436
- title: 'Project analyseren',
437
- description: 'Draait PHPStan over (een deel van) een project en geeft de bevindingen terug. ' +
438
- 'Zonder `paths` wordt geanalyseerd wat in phpstan.neon staat.',
439
- inputSchema: {
440
- project: projectArgument,
441
- paths: z
442
- .array(z.string())
443
- .optional()
444
- .describe('Paden binnen het project; leeg betekent: volg de configuratie'),
445
- level: z.string().optional().describe('Level-override, 0 t/m 10 of "max"'),
446
- config: configArgument,
447
- memoryLimit: z
448
- .string()
449
- .optional()
450
- .describe('Bijv. "1G", als PHPStan op de PHP-limiet klapt; overrulet de serverstandaard'),
451
- timeoutSeconds: z.number().int().positive().max(1800).optional(),
452
- ...windowShape,
453
- },
454
- outputSchema: analysisShape,
455
- annotations: readOnly(),
456
- }, async (args, extra) => runAnalysis(roots, args.project, (resolved) => ({
457
- options: {
458
- // Elk pad door resolveWithin, ook al komt het rechtstreeks uit de
459
- // aanroep. Ongefilterd doorgeven was op twee manieren mis: een pad
460
- // buiten het project liet PHPStan bestanden lezen waar de whitelist
461
- // juist over gaat, en een "pad" als --generate-baseline=x werd door
462
- // PHPStan als vlag gelezen en schreef dat bestand echt weg
463
- // (nagemeten). Na resolveWithin is elk pad absoluut en bestaat het,
464
- // dus het kan geen vlag meer zijn.
465
- ...(args.paths === undefined
466
- ? {}
467
- : { paths: args.paths.map((path) => resolveWithin(resolved, path)) }),
468
- ...common(resolved, args),
469
- },
470
- }), window(args), extra.signal));
471
- }
472
- function registerAnalyseChanged(server, roots) {
473
- server.registerTool('analyse_changed', {
474
- title: 'Gewijzigde bestanden analyseren',
475
- description: 'Draait PHPStan over de PHP-bestanden die in dit project gewijzigd zijn volgens git, ' +
476
- 'inclusief bestanden die nog niet toegevoegd zijn. Bedoeld om te zien of een eigen ' +
477
- 'wijziging fouten oplevert, zonder het hele project door te rekenen. Vergelijkt ' +
478
- `standaard met ${DEFAULT_BASE}; geef \`base\` mee (bijv. "main") voor de hele branch.`,
479
- inputSchema: {
480
- project: projectArgument,
481
- base: z
482
- .string()
483
- .optional()
484
- .describe(`Commit, branch of tag om tegen te vergelijken (standaard ${DEFAULT_BASE})`),
485
- staged: z
486
- .boolean()
487
- .optional()
488
- .describe('Alleen wat gestaged is (git diff --cached), voor een check vóór een commit'),
489
- level: z.string().optional().describe('Level-override, 0 t/m 10 of "max"'),
490
- config: configArgument,
491
- memoryLimit: z.string().optional(),
492
- timeoutSeconds: z.number().int().positive().max(1800).optional(),
493
- ...windowShape,
494
- },
495
- outputSchema: analysisShape,
496
- annotations: readOnly(),
497
- }, async (args, extra) => runAnalysis(roots, args.project, async (resolved) => {
498
- const changed = await changedPhpFiles(resolved, args.base, args.staged);
499
- if (changed.files.length === 0) {
500
- return { nothingToDo: describeNothingChanged(changed) };
501
- }
502
- return {
503
- options: { paths: [...changed.files], ...common(resolved, args) },
504
- heading: describeChanged(resolved, changed),
505
- };
506
- }, window(args), extra.signal));
507
- }
508
- /** Waarom er niets geanalyseerd is; de overgeslagen paden horen daar bij. */
509
- function describeNothingChanged(changed) {
510
- const wat = changed.staged ? 'gestagede' : 'gewijzigde';
511
- const lines = [`Geen ${wat} PHP-bestanden ten opzichte van ${changed.base}.`];
512
- if (changed.skipped.length > 0)
513
- lines.push('', ...skippedLines(changed));
514
- return lines.join('\n');
515
- }
516
- /** Wat er geanalyseerd is, zodat zichtbaar is waarop het rapport eronder betrekking heeft. */
517
- function describeChanged(resolved, changed) {
518
- const waar = changed.staged ? 'gestaged ten opzichte van' : 'gewijzigd ten opzichte van';
519
- const lines = [
520
- `${String(changed.files.length)} PHP-bestand(en) ${waar} ${changed.base}:`,
521
- ...changed.files.map((file) => ` ${displayPath(resolved.path, file)}`),
522
- ];
523
- if (changed.skipped.length > 0)
524
- lines.push('', ...skippedLines(changed));
525
- return lines.join('\n');
526
- }
527
- function skippedLines(changed) {
528
- return [
529
- 'Overgeslagen, dus niet meegenomen in dit rapport:',
530
- ...changed.skipped.map((entry) => ` ${entry.path}: ${entry.reason}`),
531
- ];
532
- }
533
- function registerAnalyseFile(server, roots) {
534
- server.registerTool('analyse_file', {
535
- title: 'Bestand of map analyseren',
536
- description: 'Draait PHPStan over één bestand of map binnen een project. Het pad mag relatief ' +
537
- 'aan de projectroot zijn of absoluut, maar moet binnen het project liggen.',
538
- inputSchema: {
539
- project: projectArgument,
540
- path: z.string().describe('Bestand of map binnen het project, bijv. "app/Models/User.php"'),
541
- level: z.string().optional().describe('Level-override, 0 t/m 10 of "max"'),
542
- config: configArgument,
543
- memoryLimit: z.string().optional(),
544
- timeoutSeconds: z.number().int().positive().max(1800).optional(),
545
- ...windowShape,
546
- },
547
- outputSchema: analysisShape,
548
- annotations: readOnly(),
549
- }, async (args, extra) => runAnalysis(roots, args.project, (resolved) => ({
550
- options: {
551
- paths: [resolveWithin(resolved, args.path)],
552
- ...common(resolved, args),
553
- },
554
- }), window(args), extra.signal));
555
- }
556
- /**
557
- * Zet de gedeelde argumenten om in AnalyseOptions. `config` gaat door resolveWithin:
558
- * een neon mag `bootstrapFiles` en extensies bevatten, dus een vrij te kiezen
559
- * configpad zou anders een route zijn om willekeurige PHP-code te laten uitvoeren —
560
- * langs de whitelist heen die `project` juist wél afdwingt.
561
- */
562
- function common(resolved, args) {
563
- return {
564
- ...(args.level === undefined ? {} : { level: args.level }),
565
- ...(args.config === undefined ? {} : { config: resolveWithin(resolved, args.config) }),
566
- ...(args.memoryLimit === undefined ? {} : { memoryLimit: args.memoryLimit }),
567
- ...(args.timeoutSeconds === undefined ? {} : { timeoutMs: args.timeoutSeconds * 1000 }),
568
- };
569
- }
570
- function window(args) {
571
- return {
572
- offset: args.offset,
573
- limit: args.limit,
574
- ...(args.identifiers === undefined ? {} : { identifiers: args.identifiers }),
575
- };
576
- }
577
- /**
578
- * Resolvet het project één keer en geeft het aan `plan`, zodat een tool die zelf nog
579
- * paden binnen dat project moet valideren dat op hetzelfde geresolvede project doet.
580
- * Eerder resolveerde analyse_file het project en deed runAnalysis dat daarna nog
581
- * een keer.
582
- */
583
- async function runAnalysis(roots, project, plan, window, signal) {
584
- try {
585
- const resolved = resolveProject(project, roots);
586
- const chosen = await plan(resolved);
587
- if ('nothingToDo' in chosen) {
588
- // Toch het volledige rapportschema teruggeven: een client die op
589
- // structuredContent vertrouwt moet niet ineens een ander soort antwoord
590
- // krijgen omdat er niets te doen was.
591
- const empty = buildReport({
592
- project: resolved.path,
593
- findings: [],
594
- generalErrors: [],
595
- warnings: [],
596
- totals: { fileErrors: 0, generalErrors: 0 },
597
- }, {});
598
- return {
599
- content: [{ type: 'text', text: chosen.nothingToDo }],
600
- structuredContent: { ...empty },
601
- };
602
- }
603
- // Het signaal komt uit het MCP-verzoek zelf: annuleert de client, dan wordt de
604
- // procesgroep opgeruimd in plaats van dat PHPStan doorrekent voor niemand.
605
- const result = await analyse(resolved, {
606
- ...chosen.options,
607
- ...(signal === undefined ? {} : { signal }),
608
- });
609
- const report = buildReport(result, window, collectNotices(resolved));
610
- const heading = chosen.heading === undefined ? '' : `${chosen.heading}\n\n`;
611
- return {
612
- content: [{ type: 'text', text: heading + renderReport(report) }],
613
- structuredContent: { ...report },
614
- };
615
- }
616
- catch (error) {
617
- return toFailure(error);
618
- }
619
- }
620
- /**
621
- * Wat deze server zelf bij een analyse te melden heeft. Nu alleen de baseline: doet
622
- * die mee, dan rapporteert PHPStan alleen wat er nieuw bij komt, en dan is een
623
- * schone uitslag geen schone code. Dat staat in de toolbeschrijving, maar niet in
624
- * het antwoord waar het model naar kijkt.
625
- */
626
- function collectNotices(project) {
627
- const baseline = describeIncludedBaseline(project);
628
- return baseline === null ? [] : [baseline];
629
- }
630
- function renderProjects(projects, problems) {
631
- const lines = [];
632
- if (projects.length === 0) {
633
- lines.push('Geen PHP-projecten gevonden onder de geconfigureerde mappen.');
634
- }
635
- else {
636
- lines.push(`${projects.length} project(en):`);
637
- for (const project of projects) {
638
- const config = project.config ?? 'geen config';
639
- const phpstan = project.hasPhpstan ? 'phpstan aanwezig' : 'phpstan NIET geïnstalleerd';
640
- const git = project.hasGit ? 'git' : 'geen git';
641
- lines.push(`- ${project.path} (${phpstan}, ${config}, ${git})`);
642
- }
643
- }
644
- if (problems.length > 0) {
645
- lines.push('', 'Overgeslagen regels uit de configuratie:');
646
- for (const problem of problems)
647
- lines.push(`- ${problem.root}: ${problem.problem}`);
648
- }
649
- return lines.join('\n');
650
- }
651
- /**
652
- * Analyseren verandert de broncode niet. PHPStan schrijft wel zijn resultcache weg,
653
- * maar dat is geen wijziging waar de gebruiker iets van merkt.
654
- */
655
- function readOnly() {
656
- return { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
657
- }
658
- /**
659
- * Verandert wel iets, maar niet aan de broncode. `idempotentHint` staat aan omdat
660
- * twee keer legen hetzelfde oplevert; `destructiveHint` omdat er data weggaat die
661
- * PHPStan daarna opnieuw moet opbouwen.
662
- */
663
- function clears() {
664
- return { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false };
665
- }
666
- /**
667
- * Schrijft een bestand in het project. Niet idempotent: draai je het opnieuw nadat
668
- * er fouten bij zijn gekomen, dan staat er iets anders in.
669
- */
670
- function writes() {
671
- return { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false };
672
- }
673
- function toFailure(error) {
674
- if (error instanceof ProjectError ||
675
- error instanceof PhpstanError ||
676
- error instanceof BaselineError ||
677
- error instanceof GitError ||
678
- error instanceof TmpDirError) {
679
- return failure(error.message);
680
- }
681
- throw error;
682
- }
683
- function failure(text) {
684
- return { isError: true, content: [{ type: 'text', text }] };
685
- }
686
19
  //# sourceMappingURL=tools.js.map