phpstan-mcp-mx 0.14.0 → 0.16.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/CHANGELOG.md +28 -0
- package/README.md +449 -365
- package/dist/baseline.d.ts +23 -2
- package/dist/baseline.js +32 -6
- package/dist/baseline.js.map +1 -1
- package/dist/changed.d.ts +47 -0
- package/dist/changed.js +170 -1
- package/dist/changed.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +20 -2
- package/dist/config.js.map +1 -1
- package/dist/defaults.js +2 -2
- package/dist/defaults.js.map +1 -1
- package/dist/phpstan/parameters.d.ts +41 -0
- package/dist/phpstan/parameters.js +66 -0
- package/dist/phpstan/parameters.js.map +1 -0
- package/dist/phpstan/process.d.ts +6 -0
- package/dist/phpstan/process.js +8 -1
- package/dist/phpstan/process.js.map +1 -1
- package/dist/phpstan.d.ts +6 -1
- package/dist/phpstan.js +100 -28
- package/dist/phpstan.js.map +1 -1
- package/dist/report.d.ts +2 -0
- package/dist/report.js +24 -3
- package/dist/report.js.map +1 -1
- package/dist/runner.d.ts +19 -4
- package/dist/runner.js +48 -18
- package/dist/runner.js.map +1 -1
- package/dist/tmpdir.d.ts +7 -0
- package/dist/tmpdir.js +21 -2
- package/dist/tmpdir.js.map +1 -1
- package/dist/tools/analysis.js +44 -3
- package/dist/tools/analysis.js.map +1 -1
- package/dist/tools/inspect.js +59 -11
- package/dist/tools/inspect.js.map +1 -1
- package/dist/tools/prompts.d.ts +12 -4
- package/dist/tools/prompts.js +302 -58
- package/dist/tools/prompts.js.map +1 -1
- package/dist/tools/shared.d.ts +4 -0
- package/dist/tools/shared.js +14 -4
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools.js +1 -1
- package/dist/tools.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,42 +1,45 @@
|
|
|
1
1
|
# phpstan-mcp-mx
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
An MCP server that exposes [PHPStan](https://phpstan.org) as tools for Claude. One server
|
|
4
|
+
serves several PHP projects at once; you decide which ones.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
The server's own output — messages, tool descriptions, prompts and reports — is in Dutch.
|
|
7
|
+
The examples below show that output verbatim, so you can recognise it when you see it.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
The server runs as a separate process that your MCP client starts. Install it globally once
|
|
12
|
+
and then register it with the client you use — Claude Code and Claude Desktop each keep their
|
|
13
|
+
own list, so you do this separately for both.
|
|
11
14
|
|
|
12
15
|
```bash
|
|
13
16
|
npm install -g phpstan-mcp-mx
|
|
14
17
|
```
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
Below, replace `/path/to/projects` everywhere with the directory that holds your PHP
|
|
20
|
+
projects. It is required; see [Configuration](#configuration) for why.
|
|
18
21
|
|
|
19
22
|
### Claude Code
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
With `--scope user` the server is available in all your projects — which is what you want
|
|
25
|
+
from one server that serves several projects.
|
|
23
26
|
|
|
24
27
|
```bash
|
|
25
28
|
claude mcp add phpstan --scope user \
|
|
26
|
-
--env PHPSTAN_MCP_ROOTS=/
|
|
29
|
+
--env PHPSTAN_MCP_ROOTS=/path/to/projects \
|
|
27
30
|
-- phpstan-mcp-mx
|
|
28
31
|
```
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
Without a global installation you can also run it straight from the registry; that does cost
|
|
34
|
+
an npx resolution at every session start:
|
|
32
35
|
|
|
33
36
|
```bash
|
|
34
37
|
claude mcp add phpstan --scope user \
|
|
35
|
-
--env PHPSTAN_MCP_ROOTS=/
|
|
38
|
+
--env PHPSTAN_MCP_ROOTS=/path/to/projects \
|
|
36
39
|
-- npx -y phpstan-mcp-mx
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
To check that it worked:
|
|
40
43
|
|
|
41
44
|
```bash
|
|
42
45
|
claude mcp get phpstan
|
|
@@ -49,77 +52,77 @@ phpstan:
|
|
|
49
52
|
Type: stdio
|
|
50
53
|
Command: phpstan-mcp-mx
|
|
51
54
|
Environment:
|
|
52
|
-
PHPSTAN_MCP_ROOTS=/
|
|
55
|
+
PHPSTAN_MCP_ROOTS=/path/to/projects
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
`claude mcp get`
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
`claude mcp get` and `claude mcp list` actually connect to the server instead of just reading
|
|
59
|
+
back the configuration — so they tell you whether it really starts. Inside a session, `/mcp`
|
|
60
|
+
does the same. Then ask for the project list: if it comes back empty while the connection is
|
|
61
|
+
up, your roots point to the wrong directory and there is nothing wrong with the server
|
|
62
|
+
itself.
|
|
60
63
|
|
|
61
64
|
### Claude Desktop
|
|
62
65
|
|
|
63
|
-
Desktop
|
|
66
|
+
Desktop reads its servers from `claude_desktop_config.json`:
|
|
64
67
|
|
|
65
|
-
|
|
|
68
|
+
| operating system | path |
|
|
66
69
|
| --- | --- |
|
|
67
70
|
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
68
71
|
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
|
|
69
72
|
| Linux | `~/.config/Claude/claude_desktop_config.json` |
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
If that file doesn't exist yet, create it yourself:
|
|
72
75
|
|
|
73
76
|
```json
|
|
74
77
|
{
|
|
75
78
|
"mcpServers": {
|
|
76
79
|
"phpstan": {
|
|
77
80
|
"command": "phpstan-mcp-mx",
|
|
78
|
-
"env": { "PHPSTAN_MCP_ROOTS": "/
|
|
81
|
+
"env": { "PHPSTAN_MCP_ROOTS": "/path/to/projects" }
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
85
|
```
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
If there already is an `mcpServers` block, add `phpstan` next to the existing entries instead
|
|
88
|
+
of replacing the file — otherwise you overwrite the servers that are already in it.
|
|
86
89
|
|
|
87
|
-
Desktop
|
|
88
|
-
|
|
89
|
-
|
|
90
|
+
Desktop only reads that file at startup, and on macOS closing the window is not quitting:
|
|
91
|
+
quit the app completely (⌘Q) and start it again. After that, `phpstan` shows up among the
|
|
92
|
+
available tools in the conversation.
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
`claude mcp add-from-claude-desktop`
|
|
94
|
+
If you have already set up Desktop and want the same in Claude Code,
|
|
95
|
+
`claude mcp add-from-claude-desktop` copies those servers over (macOS and WSL only).
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
If it works in your terminal but not in Desktop, go straight to the next section: a GUI app
|
|
98
|
+
does not inherit your shell profile, and that is by far the most common cause here.
|
|
96
99
|
|
|
97
|
-
###
|
|
100
|
+
### If the server won't start: PATH
|
|
98
101
|
|
|
99
|
-
|
|
100
|
-
connect"
|
|
102
|
+
If you get something like `env: node: No such file or directory`, or the server stays on
|
|
103
|
+
"failed to connect", this is almost always the cause.
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
is
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`PATH`
|
|
107
|
-
|
|
105
|
+
The `phpstan-mcp-mx` command is a script that starts with `#!/usr/bin/env node`, and `npx`
|
|
106
|
+
is itself a command that has to be found. Both work in your terminal because your shell
|
|
107
|
+
profile puts Node on `PATH`. But the MCP server is started by a *different* program, and that
|
|
108
|
+
one does not always inherit your shell profile — a GUI app on macOS often gets only a bare
|
|
109
|
+
`PATH` with `/usr/bin` and `/bin`. Node isn't there: Homebrew puts it in `/opt/homebrew/bin`,
|
|
110
|
+
and nvm, fnm, asdf and volta somewhere under your home directory.
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
The fix is to use absolute paths, so nothing needs to be looked up any more:
|
|
110
113
|
|
|
111
114
|
```bash
|
|
112
|
-
command -v node #
|
|
113
|
-
npm root -g #
|
|
115
|
+
command -v node # e.g. /opt/homebrew/bin/node
|
|
116
|
+
npm root -g # e.g. /opt/homebrew/lib/node_modules
|
|
114
117
|
```
|
|
115
118
|
|
|
116
119
|
```bash
|
|
117
120
|
claude mcp add phpstan --scope user \
|
|
118
|
-
--env PHPSTAN_MCP_ROOTS=/
|
|
121
|
+
--env PHPSTAN_MCP_ROOTS=/path/to/projects \
|
|
119
122
|
-- /opt/homebrew/bin/node /opt/homebrew/lib/node_modules/phpstan-mcp-mx/dist/index.js
|
|
120
123
|
```
|
|
121
124
|
|
|
122
|
-
|
|
125
|
+
And for Claude Desktop:
|
|
123
126
|
|
|
124
127
|
```json
|
|
125
128
|
{
|
|
@@ -127,15 +130,15 @@ En voor Claude Desktop:
|
|
|
127
130
|
"phpstan": {
|
|
128
131
|
"command": "/opt/homebrew/bin/node",
|
|
129
132
|
"args": ["/opt/homebrew/lib/node_modules/phpstan-mcp-mx/dist/index.js"],
|
|
130
|
-
"env": { "PHPSTAN_MCP_ROOTS": "/
|
|
133
|
+
"env": { "PHPSTAN_MCP_ROOTS": "/path/to/projects" }
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
```
|
|
135
138
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
You can check whether `PATH` is the problem yourself, without involving the MCP client. The
|
|
140
|
+
binary is looked up in your own shell first and then started with a bare `PATH` — otherwise
|
|
141
|
+
the command itself can't be found and you are measuring something else:
|
|
139
142
|
|
|
140
143
|
```bash
|
|
141
144
|
echo '' | env -i PATH=/usr/bin:/bin "$(command -v phpstan-mcp-mx)"
|
|
@@ -145,8 +148,8 @@ echo '' | env -i PATH=/usr/bin:/bin "$(command -v phpstan-mcp-mx)"
|
|
|
145
148
|
env: node: No such file or directory
|
|
146
149
|
```
|
|
147
150
|
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
If that is what comes out, you know enough. The same test on the absolute variant should
|
|
152
|
+
start normally:
|
|
150
153
|
|
|
151
154
|
```bash
|
|
152
155
|
echo '' | env -i PATH=/usr/bin:/bin \
|
|
@@ -157,188 +160,217 @@ echo '' | env -i PATH=/usr/bin:/bin \
|
|
|
157
160
|
phpstan-mcp 0.1.0 luistert op stdio, 0 root(s) geconfigureerd
|
|
158
161
|
```
|
|
159
162
|
|
|
160
|
-
(
|
|
161
|
-
|
|
163
|
+
(Zero roots is correct here: `env -i` also throws away `PHPSTAN_MCP_ROOTS`. All that matters
|
|
164
|
+
is *that* it starts.)
|
|
165
|
+
|
|
166
|
+
### Updating
|
|
162
167
|
|
|
163
|
-
|
|
168
|
+
A global installation does not update itself:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
npm install -g phpstan-mcp-mx@latest
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Then start a **new session** in your client. Just restarting is not enough: a server that is
|
|
175
|
+
already running keeps the code it loaded at startup, and the client holds on to the tool list
|
|
176
|
+
from the start of the session. New tools are then missing, and an argument the client doesn't
|
|
177
|
+
know yet (such as `runId`) is silently dropped.
|
|
178
|
+
|
|
179
|
+
If you register the server through `npx`, use `phpstan-mcp-mx@latest` instead of the bare
|
|
180
|
+
name; otherwise npx may keep using an older version from its cache. `server_config` shows
|
|
181
|
+
which version is running.
|
|
182
|
+
|
|
183
|
+
### From the repository
|
|
164
184
|
|
|
165
185
|
```bash
|
|
166
186
|
npm install && npm run build
|
|
167
187
|
claude mcp add phpstan --scope user \
|
|
168
|
-
--env PHPSTAN_MCP_ROOTS=/
|
|
169
|
-
-- node /
|
|
188
|
+
--env PHPSTAN_MCP_ROOTS=/path/to/projects \
|
|
189
|
+
-- node /path/to/phpstan-mcp-mx/dist/index.js
|
|
170
190
|
```
|
|
171
191
|
|
|
172
|
-
|
|
173
|
-
|
|
192
|
+
Note that you are then tied to that directory: move or delete it, and the server fails in
|
|
193
|
+
all your projects.
|
|
174
194
|
|
|
175
|
-
##
|
|
195
|
+
## Configuration
|
|
176
196
|
|
|
177
|
-
|
|
178
|
-
|
|
197
|
+
Everything can be set as an env variable in your MCP config, or in a single config file.
|
|
198
|
+
**The environment wins over the file**: the environment belongs to one client, the file
|
|
199
|
+
applies to the whole machine.
|
|
179
200
|
|
|
180
|
-
###
|
|
201
|
+
### A config file
|
|
181
202
|
|
|
182
|
-
`~/.config/phpstan-mcp/config.json` (
|
|
183
|
-
|
|
203
|
+
`~/.config/phpstan-mcp/config.json` (or `$XDG_CONFIG_HOME/phpstan-mcp/config.json`, or the
|
|
204
|
+
path in `PHPSTAN_MCP_CONFIG`):
|
|
184
205
|
|
|
185
206
|
```json
|
|
186
207
|
{
|
|
187
|
-
"roots": ["/Users/
|
|
208
|
+
"roots": ["/Users/you/projects"],
|
|
188
209
|
"memoryLimit": "2G",
|
|
189
210
|
"maxParallel": 2,
|
|
190
211
|
"tmpDir": "project",
|
|
191
212
|
"allowWrites": false,
|
|
192
213
|
"runners": {
|
|
193
|
-
"/Users/
|
|
214
|
+
"/Users/you/projects/admin": {
|
|
194
215
|
"command": ["docker", "exec", "-w", "/var/www/html", "admin-app"],
|
|
195
216
|
"workdir": "/var/www/html"
|
|
196
217
|
}
|
|
197
218
|
},
|
|
198
219
|
"projects": {
|
|
199
|
-
"/Users/
|
|
220
|
+
"/Users/you/projects/shop": { "memoryLimit": "4G", "timeoutSeconds": 900 }
|
|
200
221
|
}
|
|
201
222
|
}
|
|
202
223
|
```
|
|
203
224
|
|
|
204
|
-
|
|
205
|
-
in
|
|
225
|
+
That makes your MCP registration a single line without `--env`, and keeps the configuration
|
|
226
|
+
in one place instead of spread over seven variables.
|
|
206
227
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
228
|
+
A file that doesn't exist is not an error: then only the environment applies. A file that
|
|
229
|
+
*does* exist but is invalid stops the server from starting — someone meant something there
|
|
230
|
+
that would otherwise silently not happen.
|
|
210
231
|
|
|
211
|
-
### `PHPSTAN_MCP_ROOTS` (
|
|
232
|
+
### `PHPSTAN_MCP_ROOTS` (required)
|
|
212
233
|
|
|
213
|
-
`PHPSTAN_MCP_ROOTS` is
|
|
214
|
-
|
|
234
|
+
`PHPSTAN_MCP_ROOTS` is required: a list of absolute paths, separated by a colon (a semicolon
|
|
235
|
+
on Windows). Everything below them may be analysed, nothing outside them.
|
|
215
236
|
|
|
216
237
|
```
|
|
217
|
-
PHPSTAN_MCP_ROOTS=/Users/
|
|
238
|
+
PHPSTAN_MCP_ROOTS=/Users/you/projects:/Users/you/work/client-x
|
|
218
239
|
```
|
|
219
240
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
`node_modules/`
|
|
223
|
-
|
|
241
|
+
A root doesn't have to be a project itself. The server searches up to four levels deep for
|
|
242
|
+
directories with a `composer.json`, stops as soon as it finds one, and skips `vendor/`,
|
|
243
|
+
`node_modules/` and hidden directories. A layout like `~/projects/<client>/<project>` works
|
|
244
|
+
with `~/projects` as the only root.
|
|
224
245
|
|
|
225
|
-
### `PHPSTAN_MCP_MAX_PARALLEL` (
|
|
246
|
+
### `PHPSTAN_MCP_MAX_PARALLEL` (optional)
|
|
226
247
|
|
|
227
|
-
|
|
248
|
+
How many analyses may run at the same time, across projects. Two by default.
|
|
228
249
|
|
|
229
250
|
```
|
|
230
251
|
PHPSTAN_MCP_MAX_PARALLEL=4
|
|
231
252
|
```
|
|
232
253
|
|
|
233
|
-
Analyses
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
`PHPSTAN_MCP_MEMORY_LIMIT=2G` is dat tien keer dat plafond.
|
|
254
|
+
Analyses within the same project always run in order — two simultaneous runs would overwrite
|
|
255
|
+
each other's result cache. This limit is about the machine as a whole: without it, a model
|
|
256
|
+
that calls ten projects after `list_projects` starts ten PHPStan processes, each spinning up
|
|
257
|
+
its own parallel workers. With `PHPSTAN_MCP_MEMORY_LIMIT=2G` that is ten times that ceiling.
|
|
238
258
|
|
|
239
|
-
|
|
240
|
-
|
|
259
|
+
An unusable value is rejected instead of silently replaced by the default, and shows up as
|
|
260
|
+
`LET OP` ("attention") in the startup line:
|
|
241
261
|
|
|
242
262
|
```
|
|
243
263
|
phpstan-mcp 0.3.1 luistert op stdio, 1 root(s) geconfigureerd, hoogstens 2 tegelijk
|
|
244
264
|
```
|
|
245
265
|
|
|
246
|
-
### `PHPSTAN_MCP_TMPDIR` (
|
|
266
|
+
### `PHPSTAN_MCP_TMPDIR` (optional)
|
|
247
267
|
|
|
248
|
-
|
|
268
|
+
Where PHPStan puts its temporary files, and therefore where the result cache ends up.
|
|
249
269
|
|
|
250
|
-
|
|
|
270
|
+
| value | meaning |
|
|
251
271
|
| --- | --- |
|
|
252
|
-
| `project` (
|
|
253
|
-
| `shared` |
|
|
272
|
+
| `project` (default) | each project gets its own directory under the system temp directory |
|
|
273
|
+
| `shared` | set nothing; whatever the system uses anyway |
|
|
254
274
|
|
|
255
|
-
|
|
256
|
-
`sys_get_temp_dir()/phpstan`,
|
|
257
|
-
`resultCache.php`.
|
|
258
|
-
|
|
259
|
-
|
|
275
|
+
The default solves a silent problem. PHPStan's `tmpDir` defaults to
|
|
276
|
+
`sys_get_temp_dir()/phpstan`, so *every* project without its own `tmpDir` in its config
|
|
277
|
+
shares a single `resultCache.php`. Two projects in a row then invalidate each other's cache,
|
|
278
|
+
and the cache — the reason a second analysis is fast — stops doing anything. Measured on two
|
|
279
|
+
real projects, with another project in between:
|
|
260
280
|
|
|
261
281
|
```
|
|
262
|
-
|
|
263
|
-
|
|
282
|
+
own directory: 1s → (other project) → 1s
|
|
283
|
+
shared directory: 5s → (other project) → 3s
|
|
264
284
|
```
|
|
265
285
|
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
The directory name also contains a hash of `composer.lock`. After a `composer update`,
|
|
287
|
+
PHPStan therefore starts in a fresh directory: a result cache from before such an update
|
|
288
|
+
used to make PHPStan crash with a message that pointed nowhere. The server leaves old
|
|
289
|
+
directories in place; the system cleans those up itself.
|
|
290
|
+
|
|
291
|
+
If you set `tmpDir` yourself in your `phpstan.neon`, that wins: a configuration you made
|
|
292
|
+
explicitly takes precedence over what this server provides.
|
|
268
293
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
294
|
+
Choose `shared` if you want the server to use the same cache as your terminal, or if setting
|
|
295
|
+
`TMPDIR` disturbs something in your project — with Larastan, an entire Laravel application
|
|
296
|
+
runs in the same process.
|
|
272
297
|
|
|
273
|
-
### `PHPSTAN_MCP_RUNNERS` (
|
|
298
|
+
### `PHPSTAN_MCP_RUNNERS` (optional)
|
|
274
299
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
—
|
|
300
|
+
Some projects cannot be analysed on the host. Larastan really boots your Laravel
|
|
301
|
+
application, and if the host lacks, say, `ext-redis`, the bootstrap fails and the analysis
|
|
302
|
+
stops — while the same repository runs fine inside its container.
|
|
278
303
|
|
|
279
|
-
|
|
304
|
+
With this variable you say, per project, where PHPStan should run:
|
|
280
305
|
|
|
281
306
|
```json
|
|
282
307
|
{
|
|
283
|
-
"/Users/
|
|
308
|
+
"/Users/you/projects/admin": {
|
|
284
309
|
"command": ["docker", "exec", "-w", "/var/www/html", "admin-app"],
|
|
285
310
|
"workdir": "/var/www/html"
|
|
286
311
|
}
|
|
287
312
|
}
|
|
288
313
|
```
|
|
289
314
|
|
|
290
|
-
`command` is
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
`/var/www/html/app/User.php`
|
|
315
|
+
`command` is the command that goes in front; `workdir` is the path at which that same
|
|
316
|
+
project directory lives inside that environment. The second one is not a detail: paths you
|
|
317
|
+
pass in are translated to it, and file paths in the response come back as host paths —
|
|
318
|
+
otherwise you get `/var/www/html/app/User.php` back and can't open that file.
|
|
319
|
+
|
|
320
|
+
`list_projects` shows which environment applies to each project, and with a runner there is
|
|
321
|
+
no need for `vendor/bin/phpstan` on the host. Everything then goes through the runner:
|
|
322
|
+
analysing, but also `diagnose_project`, `clear_result_cache` and `generate_baseline`. Only
|
|
323
|
+
`unmatched_ignores` does not work with a runner (see there).
|
|
294
324
|
|
|
295
|
-
`
|
|
296
|
-
|
|
325
|
+
`workdir` is a POSIX path, even when the host is Windows: the environment is a Linux
|
|
326
|
+
container or WSL. The project key may be written in your platform's form
|
|
327
|
+
(`C:\Users\you\admin` or `C:/Users/you/admin`, with or without a trailing slash); it is
|
|
328
|
+
compared after resolving symlinks, just like the `project` argument itself.
|
|
297
329
|
|
|
298
|
-
**
|
|
299
|
-
is
|
|
300
|
-
|
|
301
|
-
|
|
330
|
+
**The command comes from the server configuration, never from a tool call.** Like the roots,
|
|
331
|
+
this is something a human sets once: a model only picks a project, and whether an
|
|
332
|
+
environment belongs to it is already decided. If a caller could pass in the command, the
|
|
333
|
+
caller would decide what gets executed and the whitelist would be pointless.
|
|
302
334
|
|
|
303
|
-
###
|
|
335
|
+
### Why that list is required
|
|
304
336
|
|
|
305
|
-
PHPStan is
|
|
306
|
-
`bootstrapFiles`
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
337
|
+
PHPStan is not a passive parser. It loads the project's autoloader, executes
|
|
338
|
+
`bootstrapFiles` from the configuration and loads extensions as PHP code. "Analyse directory
|
|
339
|
+
X" therefore means "execute the PHP code that X's configuration points to" — and the path
|
|
340
|
+
comes from a language model. The list of roots makes what is in reach explicit and decided
|
|
341
|
+
by a human.
|
|
310
342
|
|
|
311
|
-
### `PHPSTAN_MCP_MEMORY_LIMIT` (
|
|
343
|
+
### `PHPSTAN_MCP_MEMORY_LIMIT` (optional)
|
|
312
344
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
345
|
+
If PHPStan runs into the PHP memory limit, a call can pass `memoryLimit` — but only after
|
|
346
|
+
the analysis has already failed. With this variable you set the default once, next to your
|
|
347
|
+
roots:
|
|
316
348
|
|
|
317
349
|
```
|
|
318
350
|
PHPSTAN_MCP_MEMORY_LIMIT=2G
|
|
319
351
|
```
|
|
320
352
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
353
|
+
That becomes `--memory-limit=2G` on every call that doesn't pass one itself; a `memoryLimit`
|
|
354
|
+
in the call takes precedence. Allowed is whatever PHPStan itself accepts: a number of bytes,
|
|
355
|
+
optionally with `K`, `M` or `G`, or `-1` for unlimited. Anything else is rejected with a
|
|
356
|
+
message instead of passed on, because PHPStan would crash on it for every analysis.
|
|
325
357
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
358
|
+
This is the only way to set it at the server level. PHPStan has no `phpstan.neon` parameter
|
|
359
|
+
for it (`Unexpected item 'parameters › memoryLimit'`) and no env variable of its own; the
|
|
360
|
+
alternative is `memory_limit` in your `php.ini`, which then applies to *every* PHP CLI on
|
|
361
|
+
your machine.
|
|
330
362
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
363
|
+
If one project needs more than the rest, set it for that project in `PHPSTAN_MCP_PROJECTS`
|
|
364
|
+
instead of raising this value for everyone — with `PHPSTAN_MCP_MAX_PARALLEL=2`, a
|
|
365
|
+
machine-wide 4G is two times 4G.
|
|
334
366
|
|
|
335
|
-
### `PHPSTAN_MCP_PROJECTS` (
|
|
367
|
+
### `PHPSTAN_MCP_PROJECTS` (optional)
|
|
336
368
|
|
|
337
|
-
|
|
369
|
+
Defaults for a single project, as JSON — or under `projects` in the config file:
|
|
338
370
|
|
|
339
371
|
```json
|
|
340
372
|
{
|
|
341
|
-
"/Users/
|
|
373
|
+
"/Users/you/projects/shop": {
|
|
342
374
|
"memoryLimit": "4G",
|
|
343
375
|
"timeoutSeconds": 900,
|
|
344
376
|
"config": "phpstan-ci.neon"
|
|
@@ -346,222 +378,251 @@ Standaarden voor één project, als JSON — of onder `projects` in het configbe
|
|
|
346
378
|
}
|
|
347
379
|
```
|
|
348
380
|
|
|
349
|
-
|
|
|
381
|
+
| Key | Meaning |
|
|
350
382
|
| --- | --- |
|
|
351
|
-
| `memoryLimit` | `--memory-limit`
|
|
352
|
-
| `timeoutSeconds` | timeout
|
|
353
|
-
| `config` |
|
|
383
|
+
| `memoryLimit` | `--memory-limit` for this project, in the same form as `PHPSTAN_MCP_MEMORY_LIMIT` |
|
|
384
|
+
| `timeoutSeconds` | timeout for this project, from 1 up to and including 1800 |
|
|
385
|
+
| `config` | config file inside the project, if PHPStan doesn't find it by itself |
|
|
354
386
|
|
|
355
|
-
|
|
356
|
-
|
|
387
|
+
The order is always the same: what the call passes, then the project default, then the
|
|
388
|
+
server-wide one. A default is therefore not a ceiling.
|
|
357
389
|
|
|
358
|
-
`config`
|
|
359
|
-
project
|
|
360
|
-
project:
|
|
390
|
+
`config` goes through the same path check as a `config` from a call and must lie inside the
|
|
391
|
+
project; a neon file can cause PHP code to be executed. It counts everywhere as *the* config
|
|
392
|
+
of the project: for the notice that a baseline is included and for `unmatched_ignores`.
|
|
361
393
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
394
|
+
An unknown key is rejected, not ignored: `memorylimit` with a lowercase l would otherwise
|
|
395
|
+
silently do nothing. `list_projects` and `server_config` show which defaults apply, and a
|
|
396
|
+
broken value is listed there as a problem — the project list keeps working.
|
|
365
397
|
|
|
366
398
|
## Tools
|
|
367
399
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
400
|
+
Every tool expects an explicit `project` path. The server has no "current project": that
|
|
401
|
+
would be state the model doesn't see in its context, with the wrong project silently
|
|
402
|
+
analysed as a result.
|
|
371
403
|
|
|
372
404
|
### `list_projects`
|
|
373
405
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
406
|
+
Lists the projects that are in reach, with for each project whether PHPStan is installed and
|
|
407
|
+
which config file it has. Start here — the `path` from the result is what the other tools
|
|
408
|
+
expect as `project`.
|
|
377
409
|
|
|
378
410
|
### `analyse_project`
|
|
379
411
|
|
|
380
|
-
| Argument |
|
|
412
|
+
| Argument | Required | Meaning |
|
|
381
413
|
| --- | --- | --- |
|
|
382
|
-
| `project` |
|
|
383
|
-
| `paths` |
|
|
384
|
-
| `level` |
|
|
385
|
-
| `config` |
|
|
386
|
-
| `memoryLimit` |
|
|
387
|
-
| `timeoutSeconds` |
|
|
388
|
-
| `identifiers` |
|
|
389
|
-
| `offset` / `limit` |
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
414
|
+
| `project` | yes | absolute path to the project root |
|
|
415
|
+
| `paths` | no | paths inside the project; empty means: follow `phpstan.neon` |
|
|
416
|
+
| `level` | no | level override, `0` to `10` or `max` |
|
|
417
|
+
| `config` | no | a different config file, e.g. `phpstan-strict.neon`; must lie inside the project |
|
|
418
|
+
| `memoryLimit` | no | e.g. `1G`, if PHPStan hits the PHP memory limit |
|
|
419
|
+
| `timeoutSeconds` | no | 300 by default |
|
|
420
|
+
| `identifiers` | no | only these identifiers in the page, e.g. `["argument.type"]` |
|
|
421
|
+
| `offset` / `limit` | no | which part of the findings you get back |
|
|
422
|
+
| `runId` | no | from an earlier response: the next page from the same result (see [What you get back](#what-you-get-back)) |
|
|
423
|
+
|
|
424
|
+
`config` goes through the same path check as `project`. That is not a formality: a `.neon`
|
|
425
|
+
can point to `bootstrapFiles` and extensions, so a freely chosen config path would be a way
|
|
426
|
+
to have arbitrary PHP code executed around the whitelist.
|
|
427
|
+
|
|
428
|
+
`identifiers` only filters the *page*; the summary keeps counting everything, so you can
|
|
429
|
+
still see what you left out. A value matches exactly (`argument.type`) or as a group
|
|
430
|
+
(`missingType` matches `missingType.return` and `missingType.parameter`, but `argument` does
|
|
431
|
+
not match `argumentx.something`).
|
|
399
432
|
|
|
400
433
|
### `analyse_file`
|
|
401
434
|
|
|
402
|
-
|
|
403
|
-
|
|
435
|
+
Same arguments, plus a required `path` — a single file or directory inside the project. The
|
|
436
|
+
path may be relative to the project root or absolute, but it has to lie inside it.
|
|
404
437
|
|
|
405
438
|
### `analyse_changed`
|
|
406
439
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
440
|
+
Analyses the PHP files that git considers changed. Meant for checking whether your own
|
|
441
|
+
change introduces errors, without analysing the whole project — and without the response
|
|
442
|
+
filling up with what has been there for years.
|
|
410
443
|
|
|
411
|
-
| Argument |
|
|
444
|
+
| Argument | Required | Meaning |
|
|
412
445
|
| --- | --- | --- |
|
|
413
|
-
| `project` |
|
|
414
|
-
| `base` |
|
|
415
|
-
| `staged` |
|
|
416
|
-
| `onlyChangedLines` |
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
446
|
+
| `project` | yes | absolute path to the project root |
|
|
447
|
+
| `base` | no | commit, branch or tag to compare against (`HEAD` by default) |
|
|
448
|
+
| `staged` | no | only what is staged (`git diff --cached`), for a check before a commit |
|
|
449
|
+
| `onlyChangedLines` | no | only findings on lines that were changed |
|
|
450
|
+
|
|
451
|
+
Otherwise the same arguments as `analyse_project`, except `paths`. With `base=main` you get
|
|
452
|
+
the whole branch instead of just your uncommitted work.
|
|
453
|
+
|
|
454
|
+
Each finding says with `onChangedLine` whether it is on a changed line; in the text it is
|
|
455
|
+
prefixed with a `+`. A changed file often also contains errors that were already there, and
|
|
456
|
+
without that distinction you either start fixing those or stop too early. Keep in mind: a
|
|
457
|
+
finding on an unchanged line can still follow from your change — a modified signature causes
|
|
458
|
+
errors at the call sites, and you didn't touch those lines.
|
|
459
|
+
|
|
460
|
+
Two things it includes that are easy to miss: **new files not yet added to git** (`git diff`
|
|
461
|
+
doesn't know them, so there is a second query for them) and **nothing from
|
|
462
|
+
`resources/views`** — a `.blade.php` ends in `.php`, but it is a template that PHPStan never
|
|
463
|
+
sees in a normal run, because the config doesn't list that directory in `paths`.
|
|
464
|
+
|
|
465
|
+
Every path git reports goes through the same check as a path the model supplies itself.
|
|
466
|
+
Whatever doesn't pass — a broken symlink, a file pointing outside the project — is listed as
|
|
467
|
+
"skipped" in the response instead of silently dropping out.
|
|
468
|
+
|
|
469
|
+
If the project is not under git, the tool says so and points to `analyse_project` with
|
|
470
|
+
explicit `paths`.
|
|
471
|
+
|
|
472
|
+
**What the change hides** is listed under "Ter info" (for your information): new
|
|
473
|
+
`@phpstan-ignore` comments (with file, line, identifier and reason) and ignored errors added
|
|
474
|
+
to a baseline that is included — even if no PHP file changed otherwise. An ignore whose reason
|
|
475
|
+
is the only thing that changed does not count as new. The notice doesn't pass judgement: an
|
|
476
|
+
ignore can be justified, the point is that it doesn't happen silently. Ignored errors that
|
|
477
|
+
disappeared from the baseline are listed too.
|
|
438
478
|
|
|
439
479
|
### `server_config`
|
|
440
480
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
481
|
+
Returns what *this* server is running with: the project directories in reach, the default
|
|
482
|
+
memory limit, how many analyses may run at once, where temporary files go and whether
|
|
483
|
+
writing tools are enabled.
|
|
444
484
|
|
|
445
485
|
```
|
|
446
|
-
phpstan-mcp 0.
|
|
486
|
+
phpstan-mcp 0.15.0
|
|
447
487
|
|
|
448
488
|
Projectmappen:
|
|
449
|
-
/Users/
|
|
450
|
-
/
|
|
489
|
+
/Users/you/projects
|
|
490
|
+
/does/not/exist — bestaat niet
|
|
451
491
|
|
|
452
492
|
Geheugenlimiet: 2G
|
|
453
493
|
Tegelijk: 2
|
|
454
494
|
Tijdelijke map: project
|
|
455
495
|
Schrijvende tools: uit
|
|
496
|
+
Eigen omgeving: geen
|
|
497
|
+
|
|
498
|
+
Standaarden per project (een tool-argument gaat voor):
|
|
499
|
+
/Users/you/projects/shop: geheugen 4G, timeout 900s
|
|
456
500
|
```
|
|
457
501
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
502
|
+
Until now that information was only in the startup line on stderr — visible in your client
|
|
503
|
+
log, invisible to the model. A rejected setting is reported instead of omitted: if
|
|
504
|
+
`PHPSTAN_MCP_MAX_PARALLEL` is set to nonsense, that field is `null` and the reason appears
|
|
505
|
+
under "Geweigerde instellingen" (rejected settings), while the rest stays readable.
|
|
462
506
|
|
|
463
|
-
|
|
464
|
-
onder.
|
|
507
|
+
If a project has its own defaults (`PHPSTAN_MCP_PROJECTS`), they are listed per project.
|
|
465
508
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
toolantwoord.
|
|
509
|
+
Note what is *not* in there: this is a fixed list of settings and not a pass-through to the
|
|
510
|
+
environment. The same environment holds tokens; those don't belong in a tool response.
|
|
469
511
|
|
|
470
512
|
### `diagnose_project`
|
|
471
513
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
514
|
+
Asks PHPStan itself for its setup: PHP and PHPStan version, the configs that Composer
|
|
515
|
+
packages bring along (such as Larastan) and whether the turbo extension is enabled. Does not
|
|
516
|
+
run an analysis.
|
|
517
|
+
|
|
518
|
+
Use this when one project behaves differently from another, before you start doubting the
|
|
519
|
+
code. That is not theoretical: this is how it turned out that one project had a dependency
|
|
520
|
+
(`laravel/pao`) that intercepted PHPStan's output.
|
|
475
521
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
522
|
+
Below PHPStan's own text comes the configuration as PHPStan ends up using it:
|
|
523
|
+
|
|
524
|
+
```
|
|
525
|
+
Uiteindelijke configuratie, volgens PHPStan zelf:
|
|
526
|
+
Level: 10
|
|
527
|
+
Paden: app, tests
|
|
528
|
+
Uitgesloten: geen
|
|
529
|
+
Negeerregels: 1312 (verouderde worden níet gemeld; unmatched_ignores zoekt ze op)
|
|
530
|
+
Tijdelijke map: /tmp/phpstan-mcp/cms-0d03c8f34a81/phpstan
|
|
531
|
+
Bootstrap: vendor/larastan/larastan/bootstrap.php
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
The level shows up here even when it is set in a shared config from `vendor/`, and the temp
|
|
535
|
+
directory is the one `clear_result_cache` clears. If the application doesn't boot (Larastan
|
|
536
|
+
without the extensions the app needs), this part is missing, with the reason stated.
|
|
479
537
|
|
|
480
538
|
### `clear_result_cache`
|
|
481
539
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
540
|
+
Clears PHPStan's result cache, so the next analysis recomputes everything. Useful when a
|
|
541
|
+
result doesn't match the code — for example after a change to an extension or an included
|
|
542
|
+
config.
|
|
485
543
|
|
|
486
|
-
|
|
487
|
-
(`sys_get_temp_dir()/phpstan`),
|
|
488
|
-
`tmpDir
|
|
489
|
-
|
|
544
|
+
By default that cache does **not** live in the project but in the system temp directory
|
|
545
|
+
(`sys_get_temp_dir()/phpstan`), and is then shared with every other project that doesn't set
|
|
546
|
+
its own `tmpDir`. On top of that, the directory follows `TMPDIR`, so what the server clears
|
|
547
|
+
need not be the same directory your terminal uses. The response therefore names the directory
|
|
548
|
+
that was cleared:
|
|
490
549
|
|
|
491
550
|
```
|
|
492
551
|
Result cache cleared from directory:
|
|
493
552
|
/var/tmp/phpstan
|
|
494
553
|
```
|
|
495
554
|
|
|
496
|
-
|
|
555
|
+
If you want a separate cache per project, set `tmpDir` in your `phpstan.neon`.
|
|
497
556
|
|
|
498
557
|
### `baseline_summary`
|
|
499
558
|
|
|
500
|
-
|
|
501
|
-
|
|
559
|
+
Reads a project's `phpstan-baseline*.neon` and counts what is being ignored, per identifier
|
|
560
|
+
and per file. Does not run an analysis, so the answer is immediate.
|
|
561
|
+
|
|
562
|
+
Note the distinction: what is in here is exactly what `analyse_project` does *not* report. A
|
|
563
|
+
project that includes its baseline only reports the errors that are new — PHPStan already
|
|
564
|
+
makes that comparison itself.
|
|
502
565
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
566
|
+
With `path` (e.g. `"app/Models/User.php"`) you also get the entries for that one file: per
|
|
567
|
+
entry the identifier, the count and the pattern. That file doesn't have to exist any more —
|
|
568
|
+
it is precisely a deleted file that the entries doing nothing belong to.
|
|
506
569
|
|
|
507
570
|
### `unmatched_ignores`
|
|
508
571
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
`reportUnmatchedIgnoredErrors: false`,
|
|
512
|
-
|
|
572
|
+
Finds the ignore rules that no longer do anything: baseline entries for errors that were
|
|
573
|
+
fixed long ago, and entries whose `count` is higher than what still occurs. Many projects set
|
|
574
|
+
`reportUnmatchedIgnoredErrors: false`, and then this grows unnoticed — on one project, 554 of
|
|
575
|
+
the 2116 baseline entries turned out to match nothing any more.
|
|
513
576
|
|
|
514
|
-
| Argument |
|
|
577
|
+
| Argument | Required | Meaning |
|
|
515
578
|
| --- | --- | --- |
|
|
516
|
-
| `project` |
|
|
517
|
-
| `memoryLimit` |
|
|
518
|
-
| `timeoutSeconds` |
|
|
519
|
-
| `offset` / `limit` |
|
|
579
|
+
| `project` | yes | absolute path to the project root |
|
|
580
|
+
| `memoryLimit` | no | this is a full analysis, so it may need more |
|
|
581
|
+
| `timeoutSeconds` | no | 300 by default |
|
|
582
|
+
| `offset` / `limit` | no | which part of the entries you get back |
|
|
520
583
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
584
|
+
This is a full analysis, with a temporary config that includes the project's config and flips
|
|
585
|
+
the flag. That config lives in the server's temp directory and not in the project, so nothing
|
|
586
|
+
in your repository changes. PHPStan's result cache stays usable: the flag doesn't change what
|
|
587
|
+
is analysed, so the next regular analysis is just as fast.
|
|
525
588
|
|
|
526
|
-
|
|
527
|
-
in `ignoreErrors`
|
|
528
|
-
|
|
589
|
+
Each entry says whether it comes from the baseline. Patterns without a path are listed
|
|
590
|
+
separately; they live in `ignoreErrors` of your config or of a config a package brings along,
|
|
591
|
+
and a new baseline doesn't help there.
|
|
529
592
|
|
|
530
|
-
|
|
531
|
-
|
|
593
|
+
There is deliberately no `level` and no `paths`: on a different level the errors are
|
|
594
|
+
different, and with only part of the project, everything outside it looks stale.
|
|
532
595
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
596
|
+
Does not work on a project with its own execution environment (`PHPSTAN_MCP_RUNNERS`): the
|
|
597
|
+
temporary config lives on the host and can't be seen inside a container. The tool says so
|
|
598
|
+
with the reason `unsupported_with_runner`.
|
|
536
599
|
|
|
537
|
-
### `generate_baseline` (
|
|
600
|
+
### `generate_baseline` (only with write access)
|
|
538
601
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
werken.
|
|
602
|
+
Puts all current errors into a baseline, so that the next analysis only reports what is new.
|
|
603
|
+
Meant for bringing an existing project under PHPStan — not for making errors go away.
|
|
542
604
|
|
|
543
|
-
|
|
605
|
+
This tool only exists if the server was started with `PHPSTAN_MCP_ALLOW_WRITES=1`:
|
|
544
606
|
|
|
545
607
|
```bash
|
|
546
608
|
claude mcp add phpstan --scope user \
|
|
547
|
-
--env PHPSTAN_MCP_ROOTS=/
|
|
609
|
+
--env PHPSTAN_MCP_ROOTS=/path/to/projects \
|
|
548
610
|
--env PHPSTAN_MCP_ALLOW_WRITES=1 \
|
|
549
611
|
-- phpstan-mcp-mx
|
|
550
612
|
```
|
|
551
613
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
blijven read-only.
|
|
614
|
+
Without that variable it isn't registered — so it doesn't appear in the tool list at all,
|
|
615
|
+
rather than existing and refusing every call. All other tools stay read-only.
|
|
555
616
|
|
|
556
|
-
| Argument |
|
|
617
|
+
| Argument | Required | Meaning |
|
|
557
618
|
| --- | --- | --- |
|
|
558
|
-
| `project` |
|
|
559
|
-
| `file` |
|
|
560
|
-
| `allowEmpty` |
|
|
619
|
+
| `project` | yes | absolute path to the project root |
|
|
620
|
+
| `file` | no | target file inside the project (`phpstan-baseline.neon` by default) |
|
|
621
|
+
| `allowEmpty` | no | write even if there is nothing to ignore |
|
|
561
622
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
623
|
+
Otherwise the same `paths`, `level`, `config` and `memoryLimit` as an analysis. The response
|
|
624
|
+
says what still needs to happen, because a baseline that isn't included anywhere does
|
|
625
|
+
nothing:
|
|
565
626
|
|
|
566
627
|
```
|
|
567
628
|
5 fout(en) in phpstan-baseline.neon gezet.
|
|
@@ -575,23 +636,37 @@ includes:
|
|
|
575
636
|
|
|
576
637
|
## Prompts
|
|
577
638
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
`
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
639
|
+
Workflows that only combine existing tools. A prompt rather than a tool, because a tool costs
|
|
640
|
+
context in every session and a prompt only when you pick it. Every guardrail in the texts
|
|
641
|
+
comes from a trial run on a real project that went wrong without that sentence.
|
|
642
|
+
|
|
643
|
+
- **`phpstan_wijziging_nakijken`** (review your change) — review your own change with
|
|
644
|
+
`analyse_changed`, start with the findings on changed lines, and repeat until it is clean
|
|
645
|
+
(without `runId`, otherwise you see the result from before your fix). With `base` (e.g.
|
|
646
|
+
`main`) for a whole branch before a PR.
|
|
647
|
+
- **`phpstan_level_verhogen`** (raise the level) — what one level higher costs: two counts
|
|
648
|
+
with `limit: 1` and the difference per kind. The level is often set in a shared config in
|
|
649
|
+
`vendor/`, and `ignore.*` findings don't count: those are inline `@phpstan-ignore` comments
|
|
650
|
+
that have nothing to ignore on a different level.
|
|
651
|
+
- **`phpstan_baseline_afbouwen`** (shrink the baseline) — what in the baseline is stale
|
|
652
|
+
(`unmatched_ignores`), what a new baseline would hide (`analyse_project`), and the cheapest
|
|
653
|
+
real reduction. Cleaning up and hiding stay two separate things: `generate_baseline` is only
|
|
654
|
+
suggested when there is nothing to hide, and never called on its own initiative.
|
|
655
|
+
- **`phpstan_soort_wegwerken`** (clear one kind) — a clean-up round for one identifier or a
|
|
656
|
+
group such as `missingType`, with the `runId` within the round and without `runId` after
|
|
657
|
+
every fix. Without `identifier` the model picks a kind: first what can be fixed at the
|
|
658
|
+
source, otherwise a kind that sits in one or two files.
|
|
659
|
+
- **`phpstan_projecten_vergelijken`** (compare projects) — two projects side by side
|
|
660
|
+
(`diagnose_project`, level, baseline, runners and defaults), showing only the differences.
|
|
661
|
+
If no difference explains the behaviour, a stale result cache is the next suspect.
|
|
662
|
+
|
|
663
|
+
All prompts take an optional `project` (for the comparison, `projectA` and `projectB`), and a
|
|
664
|
+
client that supports completion gets a picklist from `list_projects` there.
|
|
665
|
+
|
|
666
|
+
## When a tool fails
|
|
667
|
+
|
|
668
|
+
A failed call returns two blocks: the message in plain language, and a JSON block with fields
|
|
669
|
+
you can act on.
|
|
595
670
|
|
|
596
671
|
```json
|
|
597
672
|
{
|
|
@@ -604,20 +679,20 @@ met velden waarop je kunt sturen.
|
|
|
604
679
|
}
|
|
605
680
|
```
|
|
606
681
|
|
|
607
|
-
`reason`
|
|
608
|
-
|
|
609
|
-
`
|
|
610
|
-
`
|
|
611
|
-
`
|
|
612
|
-
`unusable_output`
|
|
682
|
+
`reason` comes from a closed set, so you can match on it without reading the text:
|
|
683
|
+
`no_roots_configured`, `project_outside_roots`, `project_not_found`, `path_outside_project`,
|
|
684
|
+
`path_not_found`, `phpstan_missing`, `config_missing`, `bootstrap_failed`,
|
|
685
|
+
`baseline_unreadable`, `invalid_setting`, `memory_limit`, `timeout`, `cancelled`,
|
|
686
|
+
`git_missing`, `not_a_git_repository`, `unknown_base`, `unsupported_with_runner`,
|
|
687
|
+
`unusable_output` and `phpstan_failed`.
|
|
613
688
|
|
|
614
|
-
|
|
615
|
-
|
|
689
|
+
The fields are deliberately in `content` and not in `structuredContent`: MCP clients validate
|
|
690
|
+
the latter against the schema of the *successful* response.
|
|
616
691
|
|
|
617
|
-
##
|
|
692
|
+
## What you get back
|
|
618
693
|
|
|
619
|
-
|
|
620
|
-
|
|
694
|
+
An analysis first returns counts over *all* findings and then one page with the findings
|
|
695
|
+
themselves (50 by default, 500 at most):
|
|
621
696
|
|
|
622
697
|
```
|
|
623
698
|
25 bevinding(en) in 11 bestand(en).
|
|
@@ -635,43 +710,43 @@ src/Support/Filters/DealFilters.php
|
|
|
635
710
|
42 argument.type Parameter #1 $id expects int, string given.
|
|
636
711
|
```
|
|
637
712
|
|
|
638
|
-
|
|
713
|
+
When there are more results, the text ends with the literal follow-up call:
|
|
639
714
|
|
|
640
715
|
```
|
|
641
716
|
Nog 12 bevinding(en). Roep dezelfde tool opnieuw aan met offset=50 en runId="kX3v9aQp"; dan
|
|
642
717
|
komt de rest uit dit resultaat in plaats van uit een nieuwe analyse.
|
|
643
718
|
```
|
|
644
719
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
720
|
+
With that `runId`, the next page comes from the same result. Without it, every page would be a
|
|
721
|
+
new analysis, and if you change a file in between you get pages with different totals. Only
|
|
722
|
+
`offset`, `limit`, `identifiers` and `onlyChangedLines` may differ. If the `runId` belongs to
|
|
723
|
+
different arguments, has expired (after ten minutes) or is unknown, a new analysis follows
|
|
724
|
+
with a notice instead of an error.
|
|
650
725
|
|
|
651
|
-
|
|
652
|
-
|
|
726
|
+
A page from an earlier result states how old that result is. So after a fix, analyse
|
|
727
|
+
*without* `runId`: otherwise you see the result from before the fix.
|
|
653
728
|
|
|
654
|
-
###
|
|
729
|
+
### Cancelling
|
|
655
730
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
731
|
+
If your client cancels the request, PHPStan's whole process group is cleaned up instead of
|
|
732
|
+
it computing on for an answer nobody will read. An analysis still waiting in the queue is not
|
|
733
|
+
started.
|
|
659
734
|
|
|
660
|
-
###
|
|
735
|
+
### When the analysis is incomplete
|
|
661
736
|
|
|
662
|
-
PHPStan
|
|
663
|
-
|
|
737
|
+
PHPStan can return valid JSON and at the same time say on stderr that it didn't finish. That
|
|
738
|
+
goes at the very top of the response, before anything else:
|
|
664
739
|
|
|
665
740
|
```
|
|
666
741
|
LET OP — PHPStan meldde dit naast het resultaat:
|
|
667
742
|
- Result is incomplete because of severe errors.
|
|
668
743
|
```
|
|
669
744
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
745
|
+
That distinction is essential: without that line, an empty result can't be told apart from a
|
|
746
|
+
clean one. When there is a warning, the response also no longer reads "Geen bevindingen" (no
|
|
747
|
+
findings) but "Verder geen bevindingen" (no further findings).
|
|
673
748
|
|
|
674
|
-
|
|
749
|
+
If a baseline is included, that is stated too:
|
|
675
750
|
|
|
676
751
|
```
|
|
677
752
|
Ter info:
|
|
@@ -679,12 +754,12 @@ Ter info:
|
|
|
679
754
|
rapport. Gebruik baseline_summary om te zien wat er in staat.
|
|
680
755
|
```
|
|
681
756
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
757
|
+
That is the difference between "no findings" and "no findings, plus a thousand that aren't
|
|
758
|
+
reported". The check looks at whether your config really includes the file — a baseline
|
|
759
|
+
sitting next to `phpstan.neon` without being included does nothing.
|
|
685
760
|
|
|
686
|
-
|
|
687
|
-
|
|
761
|
+
PHPStan's own guidance for reading findings is kept separately in `instructions` and shown
|
|
762
|
+
below the report:
|
|
688
763
|
|
|
689
764
|
```
|
|
690
765
|
PHPStan zegt hierbij:
|
|
@@ -692,45 +767,54 @@ PHPStan zegt hierbij:
|
|
|
692
767
|
Do not add type casts just to silence errors.
|
|
693
768
|
```
|
|
694
769
|
|
|
695
|
-
|
|
696
|
-
|
|
770
|
+
That block is the same every run and says nothing about *this* analysis, so it doesn't count
|
|
771
|
+
as a warning — and it isn't truncated, because half a block of guidance helps nobody.
|
|
697
772
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
773
|
+
The `warnings` field is separate from `generalErrors`: the latter is PHPStan's own `errors`
|
|
774
|
+
array from the JSON. Both lists are capped at ten messages of at most 1000 characters each —
|
|
775
|
+
a failed Laravel bootstrap otherwise produces a wall of text that fills the whole response.
|
|
776
|
+
How many were left out is in `omitted`, and in the text as "nog N niet getoond" (N more not
|
|
777
|
+
shown).
|
|
703
778
|
|
|
704
|
-
##
|
|
779
|
+
## Requirements
|
|
705
780
|
|
|
706
|
-
- Node 20
|
|
707
|
-
- PHPStan
|
|
708
|
-
|
|
781
|
+
- Node 20 or newer
|
|
782
|
+
- PHPStan installed in the project itself (`vendor/bin/phpstan`); the server doesn't bring
|
|
783
|
+
its own PHPStan, because the version and the extensions belong to the project
|
|
709
784
|
|
|
710
|
-
##
|
|
785
|
+
## Known sharp edges
|
|
711
786
|
|
|
712
|
-
- **
|
|
713
|
-
`memoryLimit: "1G"`
|
|
714
|
-
|
|
715
|
-
- **Larastan
|
|
716
|
-
|
|
717
|
-
- **Analyses in
|
|
718
|
-
project
|
|
787
|
+
- **The PHP memory limit.** PHPStan easily exceeds it on a large project;
|
|
788
|
+
`memoryLimit: "1G"` usually solves that. The crash comes back as a regular error, with
|
|
789
|
+
PHPStan's own explanation attached.
|
|
790
|
+
- **Larastan boots your Laravel application.** If that fails — missing `.env`, database not
|
|
791
|
+
reachable — the analysis fails with Larastan's own explanation.
|
|
792
|
+
- **Analyses in the same project run one after another,** because they share one result cache
|
|
793
|
+
(per project, in the temp directory — not in the project itself). Different projects do run
|
|
794
|
+
in parallel.
|
|
795
|
+
- **A stale result cache** can make PHPStan crash with a message that seems to make no sense.
|
|
796
|
+
After a `composer update` that no longer happens, because the cache directory follows
|
|
797
|
+
`composer.lock`; if it happens some other way anyway, `clear_result_cache` fixes it. The
|
|
798
|
+
error message itself points to it.
|
|
799
|
+
- **If it works in your terminal but not through the client,** the server is running with a
|
|
800
|
+
different environment. The Claude app, for example, starts it without `TMPDIR`, and then the
|
|
801
|
+
per-project cache lives under `/tmp/phpstan-mcp/` instead of under your shell's temp
|
|
802
|
+
directory.
|
|
719
803
|
|
|
720
|
-
##
|
|
804
|
+
## Development
|
|
721
805
|
|
|
722
806
|
```bash
|
|
723
807
|
npm run build # tsc → dist/
|
|
724
|
-
npm run typecheck #
|
|
808
|
+
npm run typecheck # including the tests
|
|
725
809
|
npm run lint
|
|
726
810
|
npm test
|
|
727
|
-
npm run inspect # MCP Inspector
|
|
811
|
+
npm run inspect # MCP Inspector against the built server
|
|
728
812
|
```
|
|
729
813
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
814
|
+
The same four steps run in CI on Node 24, and once more on a release. `engines` promises
|
|
815
|
+
`>=20`; that lower bound was checked once in Docker when CI was set up and does not run with
|
|
816
|
+
every build.
|
|
733
817
|
|
|
734
|
-
##
|
|
818
|
+
## License
|
|
735
819
|
|
|
736
820
|
MIT
|