phpstan-mcp-mx 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.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/baseline.d.ts +52 -0
- package/dist/baseline.js +145 -0
- package/dist/baseline.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/phpstan.d.ts +45 -0
- package/dist/phpstan.js +208 -0
- package/dist/phpstan.js.map +1 -0
- package/dist/project.d.ts +46 -0
- package/dist/project.js +156 -0
- package/dist/project.js.map +1 -0
- package/dist/report.d.ts +58 -0
- package/dist/report.js +111 -0
- package/dist/report.js.map +1 -0
- package/dist/roots.d.ts +23 -0
- package/dist/roots.js +55 -0
- package/dist/roots.js.map +1 -0
- package/dist/server.d.ts +18 -0
- package/dist/server.js +27 -0
- package/dist/server.js.map +1 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +237 -0
- package/dist/tools.js.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Martijn Schimmel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# phpstan-mcp-mx
|
|
2
|
+
|
|
3
|
+
Een MCP-server die [PHPStan](https://phpstan.org) ontsluit als tools voor Claude. Eén server
|
|
4
|
+
bedient meerdere PHP-projecten tegelijk; welke dat zijn, leg je zelf vast.
|
|
5
|
+
|
|
6
|
+
## Installatie
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
claude mcp add phpstan \
|
|
10
|
+
--env PHPSTAN_MCP_ROOTS=/pad/naar/projecten \
|
|
11
|
+
-- npx -y phpstan-mcp-mx
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Of, als je de repo lokaal hebt staan:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install && npm run build
|
|
18
|
+
claude mcp add phpstan \
|
|
19
|
+
--env PHPSTAN_MCP_ROOTS=/pad/naar/projecten \
|
|
20
|
+
-- node /pad/naar/phpstan-mcp-mx/dist/index.js
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Voor Claude Desktop komt hetzelfde in `claude_desktop_config.json`:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"phpstan": {
|
|
29
|
+
"command": "npx",
|
|
30
|
+
"args": ["-y", "phpstan-mcp-mx"],
|
|
31
|
+
"env": { "PHPSTAN_MCP_ROOTS": "/pad/naar/projecten" }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuratie
|
|
38
|
+
|
|
39
|
+
`PHPSTAN_MCP_ROOTS` is verplicht: een lijst absolute paden, gescheiden door een dubbele punt
|
|
40
|
+
(een puntkomma op Windows). Alles daaronder mag geanalyseerd worden, alles daarbuiten niet.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
PHPSTAN_MCP_ROOTS=/Users/jij/projecten:/Users/jij/werk/klant-x
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Een root hoeft niet zelf een project te zijn. De server zoekt tot vier niveaus diep naar
|
|
47
|
+
mappen met een `composer.json`, stopt zodra hij er een vindt, en slaat `vendor/`,
|
|
48
|
+
`node_modules/` en verborgen mappen over. Een indeling als `~/projecten/<klant>/<project>`
|
|
49
|
+
werkt dus met `~/projecten` als enige root.
|
|
50
|
+
|
|
51
|
+
### Waarom die lijst verplicht is
|
|
52
|
+
|
|
53
|
+
PHPStan is geen passieve parser. Het laadt de autoloader van het project, voert
|
|
54
|
+
`bootstrapFiles` uit de configuratie uit en laadt extensies als PHP-code. "Analyseer map X"
|
|
55
|
+
betekent daarmee "voer de PHP-code uit die in X's configuratie staat" — en het pad komt van
|
|
56
|
+
een taalmodel. De lijst met roots maakt expliciet en menselijk vastgesteld wat binnen bereik
|
|
57
|
+
ligt.
|
|
58
|
+
|
|
59
|
+
## Tools
|
|
60
|
+
|
|
61
|
+
Elke tool verwacht een expliciet `project`-pad. De server heeft geen "huidig project": dat
|
|
62
|
+
zou toestand zijn die het model niet in zijn context ziet staan, met stilzwijgend het
|
|
63
|
+
verkeerde project als gevolg.
|
|
64
|
+
|
|
65
|
+
### `list_projects`
|
|
66
|
+
|
|
67
|
+
Somt de projecten op die binnen bereik liggen, met per project of PHPStan geïnstalleerd is
|
|
68
|
+
en welk configbestand er ligt. Begin hier — het `path` uit het resultaat is wat de andere
|
|
69
|
+
tools als `project` verwachten.
|
|
70
|
+
|
|
71
|
+
### `analyse_project`
|
|
72
|
+
|
|
73
|
+
| Argument | Verplicht | Betekenis |
|
|
74
|
+
| --- | --- | --- |
|
|
75
|
+
| `project` | ja | absoluut pad naar de projectroot |
|
|
76
|
+
| `paths` | nee | paden binnen het project; leeg betekent: volg `phpstan.neon` |
|
|
77
|
+
| `level` | nee | level-override, `0` t/m `10` of `max` |
|
|
78
|
+
| `memoryLimit` | nee | bijv. `1G`, als PHPStan op de PHP-geheugenlimiet klapt |
|
|
79
|
+
| `timeoutSeconds` | nee | standaard 300 |
|
|
80
|
+
| `offset` / `limit` | nee | welk deel van de bevindingen je terugkrijgt |
|
|
81
|
+
|
|
82
|
+
### `analyse_file`
|
|
83
|
+
|
|
84
|
+
Zelfde argumenten, plus een verplichte `path` — één bestand of map binnen het project. Het
|
|
85
|
+
pad mag relatief aan de projectroot zijn of absoluut, maar moet er wel binnen liggen.
|
|
86
|
+
|
|
87
|
+
### `baseline_summary`
|
|
88
|
+
|
|
89
|
+
Leest de `phpstan-baseline*.neon` van een project en telt wat er genegeerd wordt, per
|
|
90
|
+
identifier en per bestand. Draait geen analyse, dus het antwoord is er meteen.
|
|
91
|
+
|
|
92
|
+
Let op het onderscheid: wat hier in staat is precies wat `analyse_project` *niet* meldt. Een
|
|
93
|
+
project dat zijn baseline includet, rapporteert alleen de fouten die er nieuw bij komen —
|
|
94
|
+
die vergelijking maakt PHPStan zelf al.
|
|
95
|
+
|
|
96
|
+
## Wat je terugkrijgt
|
|
97
|
+
|
|
98
|
+
Een analyse levert eerst tellingen over álle bevindingen en daarna één pagina met de
|
|
99
|
+
bevindingen zelf (standaard 50, hoogstens 500):
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
25 bevinding(en) in 11 bestand(en).
|
|
103
|
+
|
|
104
|
+
Meest voorkomend:
|
|
105
|
+
13 argument.type
|
|
106
|
+
6 return.type
|
|
107
|
+
|
|
108
|
+
Zwaarst getroffen bestanden:
|
|
109
|
+
6 src/Support/Filters/DealFilters.php
|
|
110
|
+
|
|
111
|
+
Bevindingen 1-50 van 25:
|
|
112
|
+
|
|
113
|
+
src/Support/Filters/DealFilters.php
|
|
114
|
+
42 argument.type Parameter #1 $id expects int, string given.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Bij meer resultaten eindigt de tekst met de letterlijke vervolgaanroep (`offset=50`).
|
|
118
|
+
|
|
119
|
+
## Vereisten
|
|
120
|
+
|
|
121
|
+
- Node 20 of nieuwer
|
|
122
|
+
- PHPStan geïnstalleerd in het project zelf (`vendor/bin/phpstan`); de server brengt geen
|
|
123
|
+
eigen PHPStan mee, want de versie en de extensies horen bij het project
|
|
124
|
+
|
|
125
|
+
## Bekende scherpe randen
|
|
126
|
+
|
|
127
|
+
- **De PHP-geheugenlimiet.** PHPStan klapt daar met een groot project makkelijk overheen;
|
|
128
|
+
`memoryLimit: "1G"` lost dat meestal op. De crash komt terug als een gewone fout, met
|
|
129
|
+
PHPStans eigen uitleg erbij.
|
|
130
|
+
- **Larastan start je Laravel-applicatie.** Lukt dat niet — ontbrekende `.env`, database niet
|
|
131
|
+
bereikbaar — dan faalt de analyse met de uitleg van Larastan zelf.
|
|
132
|
+
- **Analyses in hetzelfde project draaien na elkaar,** omdat PHPStans resultcache in het
|
|
133
|
+
project ligt. Verschillende projecten draaien wel tegelijk.
|
|
134
|
+
|
|
135
|
+
## Ontwikkelen
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm run build # tsc → dist/
|
|
139
|
+
npm run typecheck # inclusief de tests
|
|
140
|
+
npm run lint
|
|
141
|
+
npm test
|
|
142
|
+
npm run inspect # MCP Inspector tegen de gebouwde server
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Licentie
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ResolvedProject } from './project.js';
|
|
2
|
+
/** Eén genegeerde fout uit een baseline-bestand. */
|
|
3
|
+
export interface BaselineEntry {
|
|
4
|
+
readonly identifier: string;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
/** Hoe vaak deze fout in dat bestand genegeerd wordt. */
|
|
7
|
+
readonly count: number;
|
|
8
|
+
}
|
|
9
|
+
/** Wat er in de baselines van één project staat. */
|
|
10
|
+
export interface BaselineSummary {
|
|
11
|
+
readonly project: string;
|
|
12
|
+
/** De gevonden baseline-bestanden, met per bestand de aantallen. */
|
|
13
|
+
readonly files: readonly {
|
|
14
|
+
file: string;
|
|
15
|
+
entries: number;
|
|
16
|
+
errors: number;
|
|
17
|
+
}[];
|
|
18
|
+
readonly totals: {
|
|
19
|
+
readonly entries: number;
|
|
20
|
+
readonly errors: number;
|
|
21
|
+
};
|
|
22
|
+
readonly byIdentifier: readonly {
|
|
23
|
+
identifier: string;
|
|
24
|
+
count: number;
|
|
25
|
+
}[];
|
|
26
|
+
readonly byPath: readonly {
|
|
27
|
+
path: string;
|
|
28
|
+
count: number;
|
|
29
|
+
}[];
|
|
30
|
+
}
|
|
31
|
+
/** Fout die als bruikbare melding naar het model teruggaat, niet als crash. */
|
|
32
|
+
export declare class BaselineError extends Error {
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Zoekt de baseline-bestanden in de projectroot. Alleen daar en niet dieper: een
|
|
36
|
+
* baseline hoort naast phpstan.neon te staan, en dieper zoeken zou de baselines
|
|
37
|
+
* van pakketten in vendor/ oppikken.
|
|
38
|
+
*/
|
|
39
|
+
export declare function findBaselineFiles(project: ResolvedProject): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Leest een baseline-bestand uit.
|
|
42
|
+
*
|
|
43
|
+
* Bewust een regelgerichte parser en geen NEON-bibliotheek: er is geen fatsoenlijke
|
|
44
|
+
* NEON-parser voor JavaScript, en dit bestand wordt door PHPStan zelf gegenereerd —
|
|
45
|
+
* de vorm ligt vast. Alleen de velden die we tellen worden gelezen; de
|
|
46
|
+
* message-regex laten we links liggen, want daar doen we niets mee.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseBaseline(content: string): BaselineEntry[];
|
|
49
|
+
/** Telt de baselines van één project bij elkaar op. */
|
|
50
|
+
export declare function summariseBaseline(project: ResolvedProject): BaselineSummary;
|
|
51
|
+
/** Schrijft een baseline-samenvatting uit als tekst voor het model. */
|
|
52
|
+
export declare function renderBaseline(summary: BaselineSummary): string;
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/** Hoeveel regels de samenvattingslijsten hoogstens tonen. */
|
|
4
|
+
const SUMMARY_ROWS = 15;
|
|
5
|
+
/** Fout die als bruikbare melding naar het model teruggaat, niet als crash. */
|
|
6
|
+
export class BaselineError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Zoekt de baseline-bestanden in de projectroot. Alleen daar en niet dieper: een
|
|
10
|
+
* baseline hoort naast phpstan.neon te staan, en dieper zoeken zou de baselines
|
|
11
|
+
* van pakketten in vendor/ oppikken.
|
|
12
|
+
*/
|
|
13
|
+
export function findBaselineFiles(project) {
|
|
14
|
+
try {
|
|
15
|
+
return readdirSync(project.path, { withFileTypes: true })
|
|
16
|
+
.filter((entry) => entry.isFile() && /^phpstan-baseline.*\.neon$/.test(entry.name))
|
|
17
|
+
.map((entry) => entry.name)
|
|
18
|
+
.sort();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Leest een baseline-bestand uit.
|
|
26
|
+
*
|
|
27
|
+
* Bewust een regelgerichte parser en geen NEON-bibliotheek: er is geen fatsoenlijke
|
|
28
|
+
* NEON-parser voor JavaScript, en dit bestand wordt door PHPStan zelf gegenereerd —
|
|
29
|
+
* de vorm ligt vast. Alleen de velden die we tellen worden gelezen; de
|
|
30
|
+
* message-regex laten we links liggen, want daar doen we niets mee.
|
|
31
|
+
*/
|
|
32
|
+
export function parseBaseline(content) {
|
|
33
|
+
const entries = [];
|
|
34
|
+
let current = null;
|
|
35
|
+
const flush = () => {
|
|
36
|
+
if (current === null)
|
|
37
|
+
return;
|
|
38
|
+
// Een blok zonder path is geen bruikbare entry; identifier ontbreekt bij
|
|
39
|
+
// baselines van vóór PHPStan 1.11 en heet dan expliciet onbekend.
|
|
40
|
+
if (current.path !== undefined) {
|
|
41
|
+
entries.push({
|
|
42
|
+
identifier: current.identifier ?? '(zonder identifier)',
|
|
43
|
+
path: current.path,
|
|
44
|
+
count: current.count ?? 1,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
current = null;
|
|
48
|
+
};
|
|
49
|
+
for (const line of content.split('\n')) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (trimmed === '-') {
|
|
52
|
+
flush();
|
|
53
|
+
current = {};
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (current === null)
|
|
57
|
+
continue;
|
|
58
|
+
const match = /^(identifier|path|count):\s*(.*)$/.exec(trimmed);
|
|
59
|
+
if (match === null)
|
|
60
|
+
continue;
|
|
61
|
+
const [, key, raw] = match;
|
|
62
|
+
const value = (raw ?? '').trim();
|
|
63
|
+
if (key === 'identifier')
|
|
64
|
+
current.identifier = unquote(value);
|
|
65
|
+
else if (key === 'path')
|
|
66
|
+
current.path = unquote(value);
|
|
67
|
+
else if (key === 'count')
|
|
68
|
+
current.count = Number.parseInt(value, 10) || 1;
|
|
69
|
+
}
|
|
70
|
+
flush();
|
|
71
|
+
return entries;
|
|
72
|
+
}
|
|
73
|
+
/** Telt de baselines van één project bij elkaar op. */
|
|
74
|
+
export function summariseBaseline(project) {
|
|
75
|
+
const files = findBaselineFiles(project);
|
|
76
|
+
if (files.length === 0) {
|
|
77
|
+
throw new BaselineError(`${project.path} heeft geen baseline (geen phpstan-baseline*.neon in de projectmap).`);
|
|
78
|
+
}
|
|
79
|
+
const perFile = [];
|
|
80
|
+
const all = [];
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const content = readFileSync(join(project.path, file), 'utf8');
|
|
83
|
+
const entries = parseBaseline(content);
|
|
84
|
+
// Een leeg resultaat uit een bestand dat wél ignoreErrors noemt betekent dat
|
|
85
|
+
// de vorm afwijkt van wat we aankunnen. Dat is iets anders dan "nul fouten".
|
|
86
|
+
if (entries.length === 0 && content.includes('ignoreErrors')) {
|
|
87
|
+
throw new BaselineError(`${file} bevat ignoreErrors maar er is geen enkele entry uit te lezen. ` +
|
|
88
|
+
'Waarschijnlijk een vorm die deze server niet kent.');
|
|
89
|
+
}
|
|
90
|
+
perFile.push({
|
|
91
|
+
file,
|
|
92
|
+
entries: entries.length,
|
|
93
|
+
errors: entries.reduce((sum, entry) => sum + entry.count, 0),
|
|
94
|
+
});
|
|
95
|
+
all.push(...entries);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
project: project.path,
|
|
99
|
+
files: perFile,
|
|
100
|
+
totals: {
|
|
101
|
+
entries: all.length,
|
|
102
|
+
errors: all.reduce((sum, entry) => sum + entry.count, 0),
|
|
103
|
+
},
|
|
104
|
+
byIdentifier: tally(all, (entry) => entry.identifier).map(([identifier, count]) => ({
|
|
105
|
+
identifier,
|
|
106
|
+
count,
|
|
107
|
+
})),
|
|
108
|
+
byPath: tally(all, (entry) => entry.path).map(([path, count]) => ({ path, count })),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Schrijft een baseline-samenvatting uit als tekst voor het model. */
|
|
112
|
+
export function renderBaseline(summary) {
|
|
113
|
+
const lines = [];
|
|
114
|
+
if (summary.totals.entries === 0) {
|
|
115
|
+
lines.push('De baseline is leeg.');
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
118
|
+
lines.push(`${summary.totals.errors} genegeerde fout(en) in ${summary.totals.entries} baseline-regel(s).`);
|
|
119
|
+
if (summary.files.length > 1) {
|
|
120
|
+
lines.push('', 'Per bestand:');
|
|
121
|
+
for (const file of summary.files)
|
|
122
|
+
lines.push(` ${pad(file.errors)} ${file.file}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push('', 'Meest genegeerd:', ...summary.byIdentifier.map((row) => ` ${pad(row.count)} ${row.identifier}`), '', 'Zwaarst belaste bestanden:', ...summary.byPath.map((row) => ` ${pad(row.count)} ${row.path}`), '', 'Dit is wat PHPStan bewust negeert. Wat analyse_project meldt staat hier juist níét in:', 'een project dat zijn baseline includet, rapporteert alleen wat er nieuw bij komt.');
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
/** De drukste regels eerst, bij gelijk aantal alfabetisch zodat de volgorde vastligt. */
|
|
128
|
+
function tally(entries, key) {
|
|
129
|
+
const counts = new Map();
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const name = key(entry);
|
|
132
|
+
counts.set(name, (counts.get(name) ?? 0) + entry.count);
|
|
133
|
+
}
|
|
134
|
+
return [...counts.entries()]
|
|
135
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
136
|
+
.slice(0, SUMMARY_ROWS);
|
|
137
|
+
}
|
|
138
|
+
function unquote(value) {
|
|
139
|
+
const match = /^'(.*)'$/.exec(value) ?? /^"(.*)"$/.exec(value);
|
|
140
|
+
return match?.[1] ?? value;
|
|
141
|
+
}
|
|
142
|
+
function pad(count) {
|
|
143
|
+
return String(count).padStart(5);
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=baseline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { loadRoots } from './roots.js';
|
|
4
|
+
import { VERSION, createServer } from './server.js';
|
|
5
|
+
async function main() {
|
|
6
|
+
const roots = loadRoots();
|
|
7
|
+
const server = createServer(roots);
|
|
8
|
+
// stdio: het protocol zelf loopt over stdout. Alles wat deze server logt moet
|
|
9
|
+
// daarom naar stderr — één console.log breekt de JSON-RPC-stroom.
|
|
10
|
+
await server.connect(new StdioServerTransport());
|
|
11
|
+
console.error(`phpstan-mcp ${VERSION} luistert op stdio, ${roots.length} root(s) geconfigureerd`);
|
|
12
|
+
}
|
|
13
|
+
main().catch((error) => {
|
|
14
|
+
console.error('phpstan-mcp kon niet starten:', error);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEpD,KAAK,UAAU,IAAI;IACjB,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IAEnC,8EAA8E;IAC9E,kEAAkE;IAClE,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IAEjD,OAAO,CAAC,KAAK,CAAC,eAAe,OAAO,uBAAuB,KAAK,CAAC,MAAM,yBAAyB,CAAC,CAAC;AACpG,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;IACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ResolvedProject } from './project.js';
|
|
2
|
+
/** Standaardtijd waarna een analyse wordt afgebroken. */
|
|
3
|
+
export declare const DEFAULT_TIMEOUT_MS = 300000;
|
|
4
|
+
/** Eén bevinding van PHPStan, gekoppeld aan een bestand. */
|
|
5
|
+
export interface Finding {
|
|
6
|
+
/** Absoluut pad zoals PHPStan het rapporteert. */
|
|
7
|
+
readonly file: string;
|
|
8
|
+
readonly line: number | null;
|
|
9
|
+
readonly message: string;
|
|
10
|
+
/** Bijv. "argument.type"; ontbreekt bij oudere PHPStan-versies. */
|
|
11
|
+
readonly identifier: string | null;
|
|
12
|
+
/** PHPStans eigen hint bij deze fout, als die er is. */
|
|
13
|
+
readonly tip: string | null;
|
|
14
|
+
}
|
|
15
|
+
/** Uitkomst van een geslaagde analyse — ook als er fouten gevonden zijn. */
|
|
16
|
+
export interface AnalysisResult {
|
|
17
|
+
readonly project: string;
|
|
18
|
+
readonly findings: readonly Finding[];
|
|
19
|
+
/** Fouten die niet aan een bestand hangen, zoals configuratieproblemen. */
|
|
20
|
+
readonly generalErrors: readonly string[];
|
|
21
|
+
readonly totals: {
|
|
22
|
+
readonly fileErrors: number;
|
|
23
|
+
readonly generalErrors: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Opties per aanroep; alles optioneel, want het project bepaalt de standaard. */
|
|
27
|
+
export interface AnalyseOptions {
|
|
28
|
+
/** Paden binnen het project. Leeg betekent: gebruik wat in de config staat. */
|
|
29
|
+
readonly paths?: readonly string[];
|
|
30
|
+
/** Level-override, 0 t/m 10 of "max". */
|
|
31
|
+
readonly level?: string;
|
|
32
|
+
readonly memoryLimit?: string;
|
|
33
|
+
readonly timeoutMs?: number;
|
|
34
|
+
}
|
|
35
|
+
/** Fout die als bruikbare melding naar het model teruggaat, niet als crash. */
|
|
36
|
+
export declare class PhpstanError extends Error {
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Draait PHPStan in één project en levert de bevindingen.
|
|
40
|
+
*
|
|
41
|
+
* Aanroepen op hetzelfde project worden op volgorde afgehandeld: PHPStans
|
|
42
|
+
* resultcache ligt ín het project, en twee gelijktijdige runs schrijven daar
|
|
43
|
+
* overheen. Verschillende projecten draaien wél parallel.
|
|
44
|
+
*/
|
|
45
|
+
export declare function analyse(project: ResolvedProject, options?: AnalyseOptions): Promise<AnalysisResult>;
|
package/dist/phpstan.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
/** Standaardtijd waarna een analyse wordt afgebroken. */
|
|
4
|
+
export const DEFAULT_TIMEOUT_MS = 300_000;
|
|
5
|
+
/** Fout die als bruikbare melding naar het model teruggaat, niet als crash. */
|
|
6
|
+
export class PhpstanError extends Error {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Draait PHPStan in één project en levert de bevindingen.
|
|
10
|
+
*
|
|
11
|
+
* Aanroepen op hetzelfde project worden op volgorde afgehandeld: PHPStans
|
|
12
|
+
* resultcache ligt ín het project, en twee gelijktijdige runs schrijven daar
|
|
13
|
+
* overheen. Verschillende projecten draaien wél parallel.
|
|
14
|
+
*/
|
|
15
|
+
export async function analyse(project, options = {}) {
|
|
16
|
+
if (project.binary === null) {
|
|
17
|
+
throw new PhpstanError(`PHPStan is niet geïnstalleerd in ${project.path} (vendor/bin/phpstan ontbreekt). ` +
|
|
18
|
+
'Draai daar eerst `composer install`.');
|
|
19
|
+
}
|
|
20
|
+
// Na de null-check is binary een string; dit maakt dat expliciet zodat run()
|
|
21
|
+
// er geen cast voor nodig heeft.
|
|
22
|
+
const installed = { ...project, binary: project.binary };
|
|
23
|
+
return enqueue(installed.path, () => run(installed, options));
|
|
24
|
+
}
|
|
25
|
+
async function run(project, options) {
|
|
26
|
+
const args = buildArgs(project, options);
|
|
27
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
28
|
+
const execution = await execute(project.binary, args, project.path, timeoutMs);
|
|
29
|
+
if (execution.spawnError !== null) {
|
|
30
|
+
throw new PhpstanError(`PHPStan kon niet gestart worden in ${project.path}: ${execution.spawnError.message}`);
|
|
31
|
+
}
|
|
32
|
+
if (execution.signal !== null) {
|
|
33
|
+
throw new PhpstanError(`De analyse van ${project.path} is na ${Math.round(timeoutMs / 1000)}s afgebroken ` +
|
|
34
|
+
`(signaal ${execution.signal}). Analyseer een kleiner deel van het project, of verhoog timeoutMs.`);
|
|
35
|
+
}
|
|
36
|
+
// Eerst proberen te parsen, pas daarna naar de exit code kijken: PHPStan geeft
|
|
37
|
+
// ook exit code 1 als het gewoon fouten gevonden heeft, en meldt configuratie-
|
|
38
|
+
// problemen ín de JSON. De exit code alleen is dus geen betrouwbaar signaal.
|
|
39
|
+
const parsed = tryParse(execution.stdout);
|
|
40
|
+
if (parsed !== null)
|
|
41
|
+
return toResult(project.path, parsed);
|
|
42
|
+
throw new PhpstanError(describeFailure(project, execution));
|
|
43
|
+
}
|
|
44
|
+
function buildArgs(project, options) {
|
|
45
|
+
const args = ['analyse', '--error-format=json', '--no-progress', '--no-interaction', '--no-ansi'];
|
|
46
|
+
if (options.level !== undefined)
|
|
47
|
+
args.push(`--level=${options.level}`);
|
|
48
|
+
if (options.memoryLimit !== undefined)
|
|
49
|
+
args.push(`--memory-limit=${options.memoryLimit}`);
|
|
50
|
+
// Zonder config én zonder paden weet PHPStan niet wat het moet doen; dan is een
|
|
51
|
+
// duidelijke melding beter dan PHPStans eigen usage-tekst.
|
|
52
|
+
const paths = options.paths ?? [];
|
|
53
|
+
if (paths.length === 0 && project.config === null) {
|
|
54
|
+
throw new PhpstanError(`${project.path} heeft geen phpstan.neon(.dist). Geef expliciet paths mee, of maak een config aan.`);
|
|
55
|
+
}
|
|
56
|
+
args.push(...paths);
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
/** Hoe lang een gedode procesgroep de tijd krijgt voordat SIGKILL volgt. */
|
|
60
|
+
const KILL_GRACE_MS = 5_000;
|
|
61
|
+
/**
|
|
62
|
+
* Start PHPStan zonder shell. Bewust spawn en niet execFile: die laatste kapt de
|
|
63
|
+
* output af op maxBuffer (1 MB), en de JSON van een groot project gaat daar
|
|
64
|
+
* ruim overheen.
|
|
65
|
+
*
|
|
66
|
+
* `detached: true` maakt het proces leider van een eigen procesgroep, zodat een
|
|
67
|
+
* timeout de héle groep kan doden. Zonder dat blijven PHPStans parallelle workers
|
|
68
|
+
* na een timeout gewoon doorrekenen.
|
|
69
|
+
*/
|
|
70
|
+
function execute(binary, args, cwd, timeoutMs) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
// Geen `timeout`-optie van spawn: die doodt alleen het proces zelf.
|
|
73
|
+
const child = spawn(binary, args, { cwd, shell: false, detached: true });
|
|
74
|
+
const stdout = [];
|
|
75
|
+
const stderr = [];
|
|
76
|
+
let spawnError = null;
|
|
77
|
+
child.stdout.on('data', (chunk) => stdout.push(chunk));
|
|
78
|
+
child.stderr.on('data', (chunk) => stderr.push(chunk));
|
|
79
|
+
child.on('error', (error) => {
|
|
80
|
+
spawnError = error;
|
|
81
|
+
});
|
|
82
|
+
const timers = [];
|
|
83
|
+
const stopTimers = () => timers.forEach((timer) => { clearTimeout(timer); });
|
|
84
|
+
timers.push(setTimeout(() => {
|
|
85
|
+
killGroup(child.pid, 'SIGTERM');
|
|
86
|
+
// Wie op SIGTERM niet reageert, krijgt alsnog SIGKILL.
|
|
87
|
+
timers.push(setTimeout(() => { killGroup(child.pid, 'SIGKILL'); }, KILL_GRACE_MS));
|
|
88
|
+
}, timeoutMs));
|
|
89
|
+
let settled = false;
|
|
90
|
+
const finish = (code, signal) => {
|
|
91
|
+
if (settled)
|
|
92
|
+
return;
|
|
93
|
+
settled = true;
|
|
94
|
+
stopTimers();
|
|
95
|
+
resolve({
|
|
96
|
+
code,
|
|
97
|
+
signal,
|
|
98
|
+
stdout: Buffer.concat(stdout).toString('utf8'),
|
|
99
|
+
stderr: Buffer.concat(stderr).toString('utf8'),
|
|
100
|
+
spawnError,
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
// Normaal wachten we op 'close': dan is alle output binnen.
|
|
104
|
+
child.on('close', finish);
|
|
105
|
+
// Maar bij een timeout wordt alleen het proces zelf gedood, terwijl zijn
|
|
106
|
+
// kleinkinderen — PHPStan draait parallelle workers — de pipes openhouden.
|
|
107
|
+
// 'close' komt dan pas als die klaar zijn, en dan is de timeout geen timeout
|
|
108
|
+
// meer. Zodra het proces zelf door een signaal is gestopt, heeft wachten op
|
|
109
|
+
// meer output geen zin.
|
|
110
|
+
child.on('exit', (code, signal) => {
|
|
111
|
+
if (signal !== null)
|
|
112
|
+
finish(code, signal);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Doodt de hele procesgroep van `pid`. Het minteken maakt van het pid een
|
|
118
|
+
* groeps-id; dat werkt alleen omdat het proces met `detached` is gestart.
|
|
119
|
+
*/
|
|
120
|
+
function killGroup(pid, signal) {
|
|
121
|
+
if (pid === undefined)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
process.kill(-pid, signal);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// De groep kan al verdwenen zijn tussen de timer en deze aanroep; dat is
|
|
128
|
+
// precies de gewenste uitkomst, geen fout.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const findingSchema = z.object({
|
|
132
|
+
message: z.string(),
|
|
133
|
+
line: z.number().nullable().optional(),
|
|
134
|
+
identifier: z.string().nullable().optional(),
|
|
135
|
+
tip: z.string().nullable().optional(),
|
|
136
|
+
});
|
|
137
|
+
const outputSchema = z.object({
|
|
138
|
+
totals: z.object({ errors: z.number(), file_errors: z.number() }),
|
|
139
|
+
files: z.record(z.string(), z.object({ messages: z.array(findingSchema) })),
|
|
140
|
+
errors: z.array(z.string()),
|
|
141
|
+
});
|
|
142
|
+
function tryParse(stdout) {
|
|
143
|
+
if (stdout.trim() === '')
|
|
144
|
+
return null;
|
|
145
|
+
let raw;
|
|
146
|
+
try {
|
|
147
|
+
raw = JSON.parse(stdout);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
// PHP codeert een lege associatieve array als [], niet als {}. Een project
|
|
153
|
+
// zonder bevindingen levert dus "files": [] op, wat het schema anders afkeurt.
|
|
154
|
+
if (typeof raw === 'object' && raw !== null && Array.isArray(raw.files)) {
|
|
155
|
+
raw.files = {};
|
|
156
|
+
}
|
|
157
|
+
const parsed = outputSchema.safeParse(raw);
|
|
158
|
+
return parsed.success ? parsed.data : null;
|
|
159
|
+
}
|
|
160
|
+
function toResult(project, output) {
|
|
161
|
+
const findings = [];
|
|
162
|
+
for (const [file, entry] of Object.entries(output.files)) {
|
|
163
|
+
for (const message of entry.messages) {
|
|
164
|
+
findings.push({
|
|
165
|
+
file,
|
|
166
|
+
line: message.line ?? null,
|
|
167
|
+
message: message.message,
|
|
168
|
+
identifier: message.identifier ?? null,
|
|
169
|
+
tip: message.tip ?? null,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
project,
|
|
175
|
+
findings,
|
|
176
|
+
generalErrors: output.errors,
|
|
177
|
+
totals: { fileErrors: output.totals.file_errors, generalErrors: output.totals.errors },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Vertaalt een run die geen bruikbare JSON opleverde naar iets waar je wat mee kunt. */
|
|
181
|
+
function describeFailure(project, execution) {
|
|
182
|
+
const details = (execution.stderr.trim() || execution.stdout.trim()).slice(0, 2000);
|
|
183
|
+
// PHPStan meldt dit op twee manieren: PHP's eigen "Allowed memory size ...
|
|
184
|
+
// exhausted", en PHPStans eigen "reached configured PHP memory limit: 128M".
|
|
185
|
+
if (/allowed memory size|memory[_ ]limit/i.test(details)) {
|
|
186
|
+
return (`PHPStan liep in ${project.path} tegen de PHP-geheugenlimiet aan. ` +
|
|
187
|
+
'Geef memoryLimit mee (bijv. "1G") of analyseer een kleiner deel.\n\n' +
|
|
188
|
+
details);
|
|
189
|
+
}
|
|
190
|
+
return (`PHPStan gaf geen bruikbare JSON terug in ${project.path} (exit code ${execution.code ?? '?'}).\n\n` +
|
|
191
|
+
(details === '' ? 'Geen output op stdout of stderr.' : details));
|
|
192
|
+
}
|
|
193
|
+
/** Lopende analyses per projectpad, zodat aanroepen per project op volgorde gaan. */
|
|
194
|
+
const queues = new Map();
|
|
195
|
+
function enqueue(key, task) {
|
|
196
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
197
|
+
// .catch vóór .then: een mislukte vorige analyse mag de volgende niet blokkeren.
|
|
198
|
+
const result = previous.then(task, task);
|
|
199
|
+
const tail = result.catch(() => undefined);
|
|
200
|
+
queues.set(key, tail);
|
|
201
|
+
void tail.then(() => {
|
|
202
|
+
// Alleen opruimen als er ondertussen niets nieuws achteraan is gezet.
|
|
203
|
+
if (queues.get(key) === tail)
|
|
204
|
+
queues.delete(key);
|
|
205
|
+
});
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=phpstan.js.map
|