phpstan-mcp-mx 0.15.0 → 0.17.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 +22 -0
- package/README.md +451 -385
- 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/phpstan/parameters.d.ts +41 -0
- package/dist/phpstan/parameters.js +66 -0
- package/dist/phpstan/parameters.js.map +1 -0
- package/dist/phpstan.d.ts +18 -0
- package/dist/phpstan.js +84 -3
- 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/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 +57 -12
- package/dist/tools/inspect.js.map +1 -1
- package/dist/tools/prompts.js +14 -11
- 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/unmatched.d.ts +2 -2
- package/dist/unmatched.js +25 -19
- package/dist/unmatched.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,195 +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.
|
|
294
319
|
|
|
295
|
-
`list_projects`
|
|
296
|
-
|
|
297
|
-
`diagnose_project`, `clear_result_cache
|
|
298
|
-
|
|
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`, `generate_baseline` and
|
|
323
|
+
`unmatched_ignores`.
|
|
299
324
|
|
|
300
|
-
`workdir` is
|
|
301
|
-
|
|
302
|
-
`C:/Users/
|
|
303
|
-
|
|
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.
|
|
304
329
|
|
|
305
|
-
**
|
|
306
|
-
is
|
|
307
|
-
|
|
308
|
-
|
|
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.
|
|
309
334
|
|
|
310
|
-
###
|
|
335
|
+
### Why that list is required
|
|
311
336
|
|
|
312
|
-
PHPStan is
|
|
313
|
-
`bootstrapFiles`
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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.
|
|
317
342
|
|
|
318
|
-
### `PHPSTAN_MCP_MEMORY_LIMIT` (
|
|
343
|
+
### `PHPSTAN_MCP_MEMORY_LIMIT` (optional)
|
|
319
344
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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:
|
|
323
348
|
|
|
324
349
|
```
|
|
325
350
|
PHPSTAN_MCP_MEMORY_LIMIT=2G
|
|
326
351
|
```
|
|
327
352
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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.
|
|
332
357
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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.
|
|
337
362
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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.
|
|
341
366
|
|
|
342
|
-
### `PHPSTAN_MCP_PROJECTS` (
|
|
367
|
+
### `PHPSTAN_MCP_PROJECTS` (optional)
|
|
343
368
|
|
|
344
|
-
|
|
369
|
+
Defaults for a single project, as JSON — or under `projects` in the config file:
|
|
345
370
|
|
|
346
371
|
```json
|
|
347
372
|
{
|
|
348
|
-
"/Users/
|
|
373
|
+
"/Users/you/projects/shop": {
|
|
349
374
|
"memoryLimit": "4G",
|
|
350
375
|
"timeoutSeconds": 900,
|
|
351
376
|
"config": "phpstan-ci.neon"
|
|
@@ -353,222 +378,254 @@ Standaarden voor één project, als JSON — of onder `projects` in het configbe
|
|
|
353
378
|
}
|
|
354
379
|
```
|
|
355
380
|
|
|
356
|
-
|
|
|
381
|
+
| Key | Meaning |
|
|
357
382
|
| --- | --- |
|
|
358
|
-
| `memoryLimit` | `--memory-limit`
|
|
359
|
-
| `timeoutSeconds` | timeout
|
|
360
|
-
| `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 |
|
|
361
386
|
|
|
362
|
-
|
|
363
|
-
|
|
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.
|
|
364
389
|
|
|
365
|
-
`config`
|
|
366
|
-
project
|
|
367
|
-
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`.
|
|
368
393
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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.
|
|
372
397
|
|
|
373
398
|
## Tools
|
|
374
399
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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.
|
|
378
403
|
|
|
379
404
|
### `list_projects`
|
|
380
405
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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`.
|
|
384
409
|
|
|
385
410
|
### `analyse_project`
|
|
386
411
|
|
|
387
|
-
| Argument |
|
|
412
|
+
| Argument | Required | Meaning |
|
|
388
413
|
| --- | --- | --- |
|
|
389
|
-
| `project` |
|
|
390
|
-
| `paths` |
|
|
391
|
-
| `level` |
|
|
392
|
-
| `config` |
|
|
393
|
-
| `memoryLimit` |
|
|
394
|
-
| `timeoutSeconds` |
|
|
395
|
-
| `identifiers` |
|
|
396
|
-
| `offset` / `limit` |
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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`).
|
|
406
432
|
|
|
407
433
|
### `analyse_file`
|
|
408
434
|
|
|
409
|
-
|
|
410
|
-
|
|
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.
|
|
411
437
|
|
|
412
438
|
### `analyse_changed`
|
|
413
439
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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.
|
|
417
443
|
|
|
418
|
-
| Argument |
|
|
444
|
+
| Argument | Required | Meaning |
|
|
419
445
|
| --- | --- | --- |
|
|
420
|
-
| `project` |
|
|
421
|
-
| `base` |
|
|
422
|
-
| `staged` |
|
|
423
|
-
| `onlyChangedLines` |
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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.
|
|
445
478
|
|
|
446
479
|
### `server_config`
|
|
447
480
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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.
|
|
451
484
|
|
|
452
485
|
```
|
|
453
|
-
phpstan-mcp 0.
|
|
486
|
+
phpstan-mcp 0.15.0
|
|
454
487
|
|
|
455
488
|
Projectmappen:
|
|
456
|
-
/Users/
|
|
457
|
-
/
|
|
489
|
+
/Users/you/projects
|
|
490
|
+
/does/not/exist — bestaat niet
|
|
458
491
|
|
|
459
492
|
Geheugenlimiet: 2G
|
|
460
493
|
Tegelijk: 2
|
|
461
494
|
Tijdelijke map: project
|
|
462
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
|
|
463
500
|
```
|
|
464
501
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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.
|
|
469
506
|
|
|
470
|
-
|
|
471
|
-
onder.
|
|
507
|
+
If a project has its own defaults (`PHPSTAN_MCP_PROJECTS`), they are listed per project.
|
|
472
508
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
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.
|
|
476
511
|
|
|
477
512
|
### `diagnose_project`
|
|
478
513
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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.
|
|
482
521
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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.
|
|
486
537
|
|
|
487
538
|
### `clear_result_cache`
|
|
488
539
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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.
|
|
492
543
|
|
|
493
|
-
|
|
494
|
-
(`sys_get_temp_dir()/phpstan`),
|
|
495
|
-
`tmpDir
|
|
496
|
-
|
|
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:
|
|
497
549
|
|
|
498
550
|
```
|
|
499
551
|
Result cache cleared from directory:
|
|
500
552
|
/var/tmp/phpstan
|
|
501
553
|
```
|
|
502
554
|
|
|
503
|
-
|
|
555
|
+
If you want a separate cache per project, set `tmpDir` in your `phpstan.neon`.
|
|
504
556
|
|
|
505
557
|
### `baseline_summary`
|
|
506
558
|
|
|
507
|
-
|
|
508
|
-
|
|
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.
|
|
509
565
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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.
|
|
513
569
|
|
|
514
570
|
### `unmatched_ignores`
|
|
515
571
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
`reportUnmatchedIgnoredErrors: false`,
|
|
519
|
-
|
|
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.
|
|
520
576
|
|
|
521
|
-
| Argument |
|
|
577
|
+
| Argument | Required | Meaning |
|
|
522
578
|
| --- | --- | --- |
|
|
523
|
-
| `project` |
|
|
524
|
-
| `memoryLimit` |
|
|
525
|
-
| `timeoutSeconds` |
|
|
526
|
-
| `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 |
|
|
527
583
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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.
|
|
532
588
|
|
|
533
|
-
|
|
534
|
-
in `ignoreErrors`
|
|
535
|
-
|
|
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.
|
|
536
592
|
|
|
537
|
-
|
|
538
|
-
|
|
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.
|
|
539
595
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
596
|
+
With an execution environment (`PHPSTAN_MCP_RUNNERS`) the temporary config goes into the
|
|
597
|
+
temporary directory of *that* environment (`${TMPDIR:-/tmp}` inside the container), because a
|
|
598
|
+
container can't see the host's temporary directory — only the project directory is mounted.
|
|
599
|
+
That needs a `sh` in the environment; without one, the tool says so with the reason
|
|
600
|
+
`unsupported_with_runner`. PHPStan can't read its config from stdin, so there is no way
|
|
601
|
+
around writing a small file somewhere; it is never written into the project itself.
|
|
543
602
|
|
|
544
|
-
### `generate_baseline` (
|
|
603
|
+
### `generate_baseline` (only with write access)
|
|
545
604
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
werken.
|
|
605
|
+
Puts all current errors into a baseline, so that the next analysis only reports what is new.
|
|
606
|
+
Meant for bringing an existing project under PHPStan — not for making errors go away.
|
|
549
607
|
|
|
550
|
-
|
|
608
|
+
This tool only exists if the server was started with `PHPSTAN_MCP_ALLOW_WRITES=1`:
|
|
551
609
|
|
|
552
610
|
```bash
|
|
553
611
|
claude mcp add phpstan --scope user \
|
|
554
|
-
--env PHPSTAN_MCP_ROOTS=/
|
|
612
|
+
--env PHPSTAN_MCP_ROOTS=/path/to/projects \
|
|
555
613
|
--env PHPSTAN_MCP_ALLOW_WRITES=1 \
|
|
556
614
|
-- phpstan-mcp-mx
|
|
557
615
|
```
|
|
558
616
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
blijven read-only.
|
|
617
|
+
Without that variable it isn't registered — so it doesn't appear in the tool list at all,
|
|
618
|
+
rather than existing and refusing every call. All other tools stay read-only.
|
|
562
619
|
|
|
563
|
-
| Argument |
|
|
620
|
+
| Argument | Required | Meaning |
|
|
564
621
|
| --- | --- | --- |
|
|
565
|
-
| `project` |
|
|
566
|
-
| `file` |
|
|
567
|
-
| `allowEmpty` |
|
|
622
|
+
| `project` | yes | absolute path to the project root |
|
|
623
|
+
| `file` | no | target file inside the project (`phpstan-baseline.neon` by default) |
|
|
624
|
+
| `allowEmpty` | no | write even if there is nothing to ignore |
|
|
568
625
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
626
|
+
Otherwise the same `paths`, `level`, `config` and `memoryLimit` as an analysis. The response
|
|
627
|
+
says what still needs to happen, because a baseline that isn't included anywhere does
|
|
628
|
+
nothing:
|
|
572
629
|
|
|
573
630
|
```
|
|
574
631
|
5 fout(en) in phpstan-baseline.neon gezet.
|
|
@@ -582,37 +639,37 @@ includes:
|
|
|
582
639
|
|
|
583
640
|
## Prompts
|
|
584
641
|
|
|
585
|
-
|
|
586
|
-
context
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
- **`phpstan_wijziging_nakijken`** —
|
|
590
|
-
|
|
591
|
-
`runId`,
|
|
592
|
-
|
|
593
|
-
- **`phpstan_level_verhogen`**
|
|
594
|
-
|
|
595
|
-
`ignore
|
|
596
|
-
|
|
597
|
-
- **`phpstan_baseline_afbouwen`** —
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
- **`phpstan_soort_wegwerken`** —
|
|
602
|
-
`missingType`,
|
|
603
|
-
`identifier`
|
|
604
|
-
|
|
605
|
-
- **`phpstan_projecten_vergelijken`** —
|
|
606
|
-
level, baseline, runners
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
##
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
642
|
+
Workflows that only combine existing tools. A prompt rather than a tool, because a tool costs
|
|
643
|
+
context in every session and a prompt only when you pick it. Every guardrail in the texts
|
|
644
|
+
comes from a trial run on a real project that went wrong without that sentence.
|
|
645
|
+
|
|
646
|
+
- **`phpstan_wijziging_nakijken`** (review your change) — review your own change with
|
|
647
|
+
`analyse_changed`, start with the findings on changed lines, and repeat until it is clean
|
|
648
|
+
(without `runId`, otherwise you see the result from before your fix). With `base` (e.g.
|
|
649
|
+
`main`) for a whole branch before a PR.
|
|
650
|
+
- **`phpstan_level_verhogen`** (raise the level) — what one level higher costs: two counts
|
|
651
|
+
with `limit: 1` and the difference per kind. The level is often set in a shared config in
|
|
652
|
+
`vendor/`, and `ignore.*` findings don't count: those are inline `@phpstan-ignore` comments
|
|
653
|
+
that have nothing to ignore on a different level.
|
|
654
|
+
- **`phpstan_baseline_afbouwen`** (shrink the baseline) — what in the baseline is stale
|
|
655
|
+
(`unmatched_ignores`), what a new baseline would hide (`analyse_project`), and the cheapest
|
|
656
|
+
real reduction. Cleaning up and hiding stay two separate things: `generate_baseline` is only
|
|
657
|
+
suggested when there is nothing to hide, and never called on its own initiative.
|
|
658
|
+
- **`phpstan_soort_wegwerken`** (clear one kind) — a clean-up round for one identifier or a
|
|
659
|
+
group such as `missingType`, with the `runId` within the round and without `runId` after
|
|
660
|
+
every fix. Without `identifier` the model picks a kind: first what can be fixed at the
|
|
661
|
+
source, otherwise a kind that sits in one or two files.
|
|
662
|
+
- **`phpstan_projecten_vergelijken`** (compare projects) — two projects side by side
|
|
663
|
+
(`diagnose_project`, level, baseline, runners and defaults), showing only the differences.
|
|
664
|
+
If no difference explains the behaviour, a stale result cache is the next suspect.
|
|
665
|
+
|
|
666
|
+
All prompts take an optional `project` (for the comparison, `projectA` and `projectB`), and a
|
|
667
|
+
client that supports completion gets a picklist from `list_projects` there.
|
|
668
|
+
|
|
669
|
+
## When a tool fails
|
|
670
|
+
|
|
671
|
+
A failed call returns two blocks: the message in plain language, and a JSON block with fields
|
|
672
|
+
you can act on.
|
|
616
673
|
|
|
617
674
|
```json
|
|
618
675
|
{
|
|
@@ -625,20 +682,20 @@ met velden waarop je kunt sturen.
|
|
|
625
682
|
}
|
|
626
683
|
```
|
|
627
684
|
|
|
628
|
-
`reason`
|
|
629
|
-
|
|
630
|
-
`
|
|
631
|
-
`
|
|
632
|
-
`
|
|
633
|
-
`unusable_output`
|
|
685
|
+
`reason` comes from a closed set, so you can match on it without reading the text:
|
|
686
|
+
`no_roots_configured`, `project_outside_roots`, `project_not_found`, `path_outside_project`,
|
|
687
|
+
`path_not_found`, `phpstan_missing`, `config_missing`, `bootstrap_failed`,
|
|
688
|
+
`baseline_unreadable`, `invalid_setting`, `memory_limit`, `timeout`, `cancelled`,
|
|
689
|
+
`git_missing`, `not_a_git_repository`, `unknown_base`, `unsupported_with_runner`,
|
|
690
|
+
`unusable_output` and `phpstan_failed`.
|
|
634
691
|
|
|
635
|
-
|
|
636
|
-
|
|
692
|
+
The fields are deliberately in `content` and not in `structuredContent`: MCP clients validate
|
|
693
|
+
the latter against the schema of the *successful* response.
|
|
637
694
|
|
|
638
|
-
##
|
|
695
|
+
## What you get back
|
|
639
696
|
|
|
640
|
-
|
|
641
|
-
|
|
697
|
+
An analysis first returns counts over *all* findings and then one page with the findings
|
|
698
|
+
themselves (50 by default, 500 at most):
|
|
642
699
|
|
|
643
700
|
```
|
|
644
701
|
25 bevinding(en) in 11 bestand(en).
|
|
@@ -656,43 +713,43 @@ src/Support/Filters/DealFilters.php
|
|
|
656
713
|
42 argument.type Parameter #1 $id expects int, string given.
|
|
657
714
|
```
|
|
658
715
|
|
|
659
|
-
|
|
716
|
+
When there are more results, the text ends with the literal follow-up call:
|
|
660
717
|
|
|
661
718
|
```
|
|
662
719
|
Nog 12 bevinding(en). Roep dezelfde tool opnieuw aan met offset=50 en runId="kX3v9aQp"; dan
|
|
663
720
|
komt de rest uit dit resultaat in plaats van uit een nieuwe analyse.
|
|
664
721
|
```
|
|
665
722
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
723
|
+
With that `runId`, the next page comes from the same result. Without it, every page would be a
|
|
724
|
+
new analysis, and if you change a file in between you get pages with different totals. Only
|
|
725
|
+
`offset`, `limit`, `identifiers` and `onlyChangedLines` may differ. If the `runId` belongs to
|
|
726
|
+
different arguments, has expired (after ten minutes) or is unknown, a new analysis follows
|
|
727
|
+
with a notice instead of an error.
|
|
671
728
|
|
|
672
|
-
|
|
673
|
-
|
|
729
|
+
A page from an earlier result states how old that result is. So after a fix, analyse
|
|
730
|
+
*without* `runId`: otherwise you see the result from before the fix.
|
|
674
731
|
|
|
675
|
-
###
|
|
732
|
+
### Cancelling
|
|
676
733
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
734
|
+
If your client cancels the request, PHPStan's whole process group is cleaned up instead of
|
|
735
|
+
it computing on for an answer nobody will read. An analysis still waiting in the queue is not
|
|
736
|
+
started.
|
|
680
737
|
|
|
681
|
-
###
|
|
738
|
+
### When the analysis is incomplete
|
|
682
739
|
|
|
683
|
-
PHPStan
|
|
684
|
-
|
|
740
|
+
PHPStan can return valid JSON and at the same time say on stderr that it didn't finish. That
|
|
741
|
+
goes at the very top of the response, before anything else:
|
|
685
742
|
|
|
686
743
|
```
|
|
687
744
|
LET OP — PHPStan meldde dit naast het resultaat:
|
|
688
745
|
- Result is incomplete because of severe errors.
|
|
689
746
|
```
|
|
690
747
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
748
|
+
That distinction is essential: without that line, an empty result can't be told apart from a
|
|
749
|
+
clean one. When there is a warning, the response also no longer reads "Geen bevindingen" (no
|
|
750
|
+
findings) but "Verder geen bevindingen" (no further findings).
|
|
694
751
|
|
|
695
|
-
|
|
752
|
+
If a baseline is included, that is stated too:
|
|
696
753
|
|
|
697
754
|
```
|
|
698
755
|
Ter info:
|
|
@@ -700,12 +757,12 @@ Ter info:
|
|
|
700
757
|
rapport. Gebruik baseline_summary om te zien wat er in staat.
|
|
701
758
|
```
|
|
702
759
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
760
|
+
That is the difference between "no findings" and "no findings, plus a thousand that aren't
|
|
761
|
+
reported". The check looks at whether your config really includes the file — a baseline
|
|
762
|
+
sitting next to `phpstan.neon` without being included does nothing.
|
|
706
763
|
|
|
707
|
-
|
|
708
|
-
|
|
764
|
+
PHPStan's own guidance for reading findings is kept separately in `instructions` and shown
|
|
765
|
+
below the report:
|
|
709
766
|
|
|
710
767
|
```
|
|
711
768
|
PHPStan zegt hierbij:
|
|
@@ -713,45 +770,54 @@ PHPStan zegt hierbij:
|
|
|
713
770
|
Do not add type casts just to silence errors.
|
|
714
771
|
```
|
|
715
772
|
|
|
716
|
-
|
|
717
|
-
|
|
773
|
+
That block is the same every run and says nothing about *this* analysis, so it doesn't count
|
|
774
|
+
as a warning — and it isn't truncated, because half a block of guidance helps nobody.
|
|
718
775
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
776
|
+
The `warnings` field is separate from `generalErrors`: the latter is PHPStan's own `errors`
|
|
777
|
+
array from the JSON. Both lists are capped at ten messages of at most 1000 characters each —
|
|
778
|
+
a failed Laravel bootstrap otherwise produces a wall of text that fills the whole response.
|
|
779
|
+
How many were left out is in `omitted`, and in the text as "nog N niet getoond" (N more not
|
|
780
|
+
shown).
|
|
724
781
|
|
|
725
|
-
##
|
|
782
|
+
## Requirements
|
|
726
783
|
|
|
727
|
-
- Node 20
|
|
728
|
-
- PHPStan
|
|
729
|
-
|
|
784
|
+
- Node 20 or newer
|
|
785
|
+
- PHPStan installed in the project itself (`vendor/bin/phpstan`); the server doesn't bring
|
|
786
|
+
its own PHPStan, because the version and the extensions belong to the project
|
|
730
787
|
|
|
731
|
-
##
|
|
788
|
+
## Known sharp edges
|
|
732
789
|
|
|
733
|
-
- **
|
|
734
|
-
`memoryLimit: "1G"`
|
|
735
|
-
|
|
736
|
-
- **Larastan
|
|
737
|
-
|
|
738
|
-
- **Analyses in
|
|
739
|
-
project
|
|
790
|
+
- **The PHP memory limit.** PHPStan easily exceeds it on a large project;
|
|
791
|
+
`memoryLimit: "1G"` usually solves that. The crash comes back as a regular error, with
|
|
792
|
+
PHPStan's own explanation attached.
|
|
793
|
+
- **Larastan boots your Laravel application.** If that fails — missing `.env`, database not
|
|
794
|
+
reachable — the analysis fails with Larastan's own explanation.
|
|
795
|
+
- **Analyses in the same project run one after another,** because they share one result cache
|
|
796
|
+
(per project, in the temp directory — not in the project itself). Different projects do run
|
|
797
|
+
in parallel.
|
|
798
|
+
- **A stale result cache** can make PHPStan crash with a message that seems to make no sense.
|
|
799
|
+
After a `composer update` that no longer happens, because the cache directory follows
|
|
800
|
+
`composer.lock`; if it happens some other way anyway, `clear_result_cache` fixes it. The
|
|
801
|
+
error message itself points to it.
|
|
802
|
+
- **If it works in your terminal but not through the client,** the server is running with a
|
|
803
|
+
different environment. The Claude app, for example, starts it without `TMPDIR`, and then the
|
|
804
|
+
per-project cache lives under `/tmp/phpstan-mcp/` instead of under your shell's temp
|
|
805
|
+
directory.
|
|
740
806
|
|
|
741
|
-
##
|
|
807
|
+
## Development
|
|
742
808
|
|
|
743
809
|
```bash
|
|
744
810
|
npm run build # tsc → dist/
|
|
745
|
-
npm run typecheck #
|
|
811
|
+
npm run typecheck # including the tests
|
|
746
812
|
npm run lint
|
|
747
813
|
npm test
|
|
748
|
-
npm run inspect # MCP Inspector
|
|
814
|
+
npm run inspect # MCP Inspector against the built server
|
|
749
815
|
```
|
|
750
816
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
817
|
+
The same four steps run in CI on Node 24, and once more on a release. `engines` promises
|
|
818
|
+
`>=20`; that lower bound was checked once in Docker when CI was set up and does not run with
|
|
819
|
+
every build.
|
|
754
820
|
|
|
755
|
-
##
|
|
821
|
+
## License
|
|
756
822
|
|
|
757
823
|
MIT
|