survos / command-bundle
Run Symfony console commands from a web interface, in the background, or as MCP tools for agents
Package info
github.com/survos/command-bundle
Type:symfony-bundle
pkg:composer/survos/command-bundle
Fund package maintenance!
Requires
- php: ^8.5
- doctrine/orm: ^3.6
- survos/field-bundle: ^2.5
- survos/kit-bundle: ^2.5
- symfony/config: ^7.4||^8.1
- symfony/console: ^7.4||^8.1
- symfony/dependency-injection: ^7.4||^8.1
- symfony/form: ^7.4||^8.1
- symfony/framework-bundle: ^7.4||^8.1
- symfony/http-kernel: ^7.4||^8.1
- symfony/messenger: ^7.4||^8.1
- symfony/uid: ^7.4||^8.1
- twig/twig: ^3.4|^4.0
Requires (Dev)
- mcp/sdk: ^0.8
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^13.0
- rector/rector: ^2.0
- survos/tui-extras-bundle: ^2.8
- symfony/security-bundle: ^7.4||^8.1
- symfony/test-pack: ^1.0
- symfony/yaml: ^7.4||^8.1
Suggests
- survos/tui-extras-bundle: Enables the `command:monitor` live TUI (symfony/tui widgets).
- symfony/mcp-bundle: Exposes #[AsAgentTool] commands (and survos_command.agent_tools) as MCP tools.
- symfony/security-bundle: Per-tool role checks and the /mcp bearer-token handler (AgentTokenHandler).
Provides
None
Conflicts
- mcp/sdk: <0.8
Replaces
None
- dev-main
- 2.34.29
- 2.34.23
- 2.34.18
- 2.34.17
- 2.34.16
- 2.33.0
- 2.18.18
- 2.18.3
- 2.18.2
- 2.10.24
- 2.10.19
- 2.10.18
- 2.10.17
- 2.10.16
- 2.10.15
- 2.10.14
- 2.10.13
- 2.10.12
- 2.10.11
- 2.10.10
- 2.10.9
- 2.10.8
- 2.10.7
- 2.10.6
- 2.10.5
- 2.10.4
- 2.10.3
- 2.10.2
- 2.10.1
- 2.10.0
- 2.9.4
- 2.9.3
- 2.9.2
- 2.9.1
- 2.9.0
- 2.8.4
- 2.8.3
- 2.8.2
- 2.8.1
- 2.8.0
- 2.7.23
- 2.7.22
- 2.7.21
- 2.7.20
- 2.7.19
- 2.7.18
- 2.7.17
- 2.7.16
- 2.7.15
- 2.7.14
- 2.7.13
- 2.7.12
- 2.7.11
- 2.7.10
- 2.7.9
- 2.7.8
- 2.7.7
- 2.7.6
- 2.7.5
- 2.7.4
- 2.7.3
- 2.7.2
- 2.7.1
- 2.7.0
- 2.6.0
- 2.5.8
- 2.5.7
- 2.5.6
- 2.5.5
- 2.5.3
- 2.5.2
- 2.5.1
- 2.5.0
- 2.4.4
- 2.4.3
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.0
- 2.2.5
- 2.2.4
- 2.2.3
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.2
- 2.1.1
- 2.0.220
- 2.0.219
- 2.0.218
- 2.0.217
- 2.0.216
- 2.0.215
- 2.0.214
- 2.0.213
- 2.0.212
- 2.0.211
- 2.0.210
- 2.0.209
- 2.0.208
- 2.0.207
- 2.0.206
- 2.0.205
- 2.0.204
- 2.0.203
- 2.0.202
- 2.0.201
- 2.0.200
- 2.0.199
- 2.0.198
- 2.0.197
- 2.0.196
- 2.0.195
- 2.0.194
- 2.0.193
- 2.0.192
- 2.0.191
- 2.0.190
- 2.0.189
- 2.0.188
- 2.0.186
- 2.0.185
- 2.0.184
- 2.0.183
- 2.0.182
- 2.0.181
- 2.0.180
- 2.0.179
- 2.0.178
- 2.0.177
- 2.0.176
- 2.0.175
- 2.0.174
- 2.0.173
- 2.0.172
- 2.0.171
- 2.0.170
- 2.0.169
- 2.0.168
- 2.0.167
- 2.0.166
- 2.0.165
- 2.0.164
- 2.0.163
- 2.0.162
- 2.0.161
- 2.0.160
- 2.0.159
- 2.0.158
- 2.0.156
- 2.0.155
- 2.0.154
- 2.0.146
- 2.0.145
- 2.0.144
- 2.0.143
- 2.0.142
- 2.0.141
- 2.0.140
- 2.0.139
- 2.0.138
- 2.0.136
- 2.0.135
- 2.0.134
- 2.0.133
- 2.0.132
- 2.0.131
- 2.0.130
- 2.0.129
- 2.0.128
- 2.0.127
- 2.0.126
- 2.0.125
- 2.0.124
- 2.0.123
- 2.0.122
- 2.0.121
- 2.0.120
- 2.0.119
- 2.0.117
- 2.0.116
- 2.0.115
- 2.0.114
- 2.0.113
- 2.0.112
- 2.0.111
- 2.0.110
- 2.0.109
- 2.0.108
- 2.0.107
- 2.0.106
- 2.0.105
- 2.0.104
- 2.0.103
- 2.0.102
- 2.0.101
- 2.0.100
- 2.0.99
- 2.0.98
- 2.0.97
- 2.0.96
- 2.0.95
- 2.0.94
- 2.0.93
- 2.0.92
- 2.0.91
- 2.0.90
- 2.0.89
- 2.0.88
- 2.0.87
- 2.0.86
- 2.0.85
- 2.0.84
- 2.0.83
- 2.0.82
- 2.0.81
- 2.0.80
- 2.0.79
- 2.0.78
- 2.0.77
- 2.0.76
- 2.0.75
- 2.0.74
- 2.0.73
- 2.0.72
- 2.0.71
- 2.0.70
- 2.0.69
- 2.0.68
- 2.0.67
- 2.0.66
- 2.0.65
- 2.0.64
- 2.0.63
- 2.0.62
- 2.0.61
- 2.0.60
- 2.0.59
- 2.0.58
- 2.0.57
- 2.0.56
- 2.0.55
- 2.0.54
- 2.0.53
- 2.0.51
- 2.0.50
- 2.0.49
- 2.0.48
- 2.0.47
- 2.0.46
- 2.0.45
- 2.0.44
- 2.0.43
- 2.0.42
- 2.0.41
- 2.0.40
- 2.0.39
- 2.0.38
- 2.0.37
- 2.0.36
- 2.0.35
- 2.0.34
- 2.0.33
- 2.0.32
- 2.0.31
- 2.0.30
- 2.0.29
- 2.0.28
- 2.0.27
- 2.0.26
- 2.0.25
- 2.0.24
- 2.0.23
- 2.0.22
- 2.0.21
- 2.0.20
- 2.0.19
- 2.0.18
- 2.0.17
- 2.0.16
- 2.0.15
- 2.0.14
- 2.0.13
- 1.6.44
- 1.6.43
- 1.6.42
- 1.6.41
- 1.6.40
- 1.6.39
- 1.6.38
- 1.6.37
- 1.6.36
- 1.6.35
- 1.6.34
- 1.6.33
- 1.6.32
- 1.6.31
- 1.6.30
- 1.6.29
- 1.6.28
- 1.6.27
- 1.6.26
- 1.6.25
- 1.6.24
- 1.6.23
- 1.6.22
- 1.6.21
- 1.6.20
- 1.6.19
- 1.6.18
- 1.6.17
- 1.6.16
- 1.6.15
- 1.6.14
- 1.6.13
- 1.6.12
- 1.6.11
- 1.6.10
- 1.6.9
- 1.6.8
- 1.6.7
- 1.6.6
- 1.6.5
- 1.6.4
- 1.6.3
- 1.6.2
- 1.6.1
- 1.6.0
- 1.5.529
- 1.5.528
- 1.5.527
- 1.5.526
- 1.5.525
- 1.5.524
- 1.5.523
- 1.5.522
- 1.5.521
- 1.5.520
- 1.5.519
- 1.5.518
- 1.5.517
- 1.5.516
- 1.5.515
- 1.5.514
- 1.5.513
- 1.5.512
- 1.5.511
- 1.5.510
- 1.5.509
- 1.5.508
- 1.5.507
- 1.5.506
- 1.5.505
- 1.5.504
- 1.5.503
- 1.5.502
- 1.5.501
- 1.5.500
- 1.5.499
- 1.5.498
- 1.5.497
- 1.5.496
- 1.5.495
- 1.5.494
- 1.5.493
- 1.5.492
- 1.5.491
- 1.5.490
- 1.5.489
- 1.5.488
- 1.5.487
- 1.5.486
- 1.5.485
- 1.5.484
- 1.5.483
- 1.5.482
- 1.5.481
- 1.5.480
- 1.5.479
- 1.5.478
- 1.5.477
- 1.5.476
- 1.5.475
- 1.5.474
- 1.5.473
- 1.5.472
- 1.5.471
- 1.5.470
- 1.5.469
- 1.5.468
- 1.5.467
- 1.5.466
- 1.5.465
- 1.5.464
- 1.5.463
- 1.5.462
- 1.5.461
- 1.5.460
- 1.5.459
- 1.5.458
- 1.5.457
- 1.5.456
- 1.5.455
- 1.5.454
- 1.5.453
- 1.5.452
- 1.5.451
- 1.5.450
- 1.5.449
- 1.5.448
- 1.5.447
- 1.5.446
- 1.5.445
- 1.5.444
- 1.5.443
- 1.5.442
- 1.5.441
- 1.5.440
- 1.5.439
- 1.5.438
- 1.5.437
- 1.5.436
- 1.5.435
- 1.5.434
- 1.5.433
- 1.5.432
- 1.5.431
- 1.5.430
- 1.5.429
- 1.5.428
- 1.5.427
- 1.5.426
- 1.5.425
- 1.5.424
- 1.5.423
- 1.5.422
- 1.5.421
- 1.5.420
- 1.5.419
- 1.5.418
- 1.5.417
- 1.5.416
- 1.5.415
- 1.5.414
- 1.5.413
- 1.5.412
- 1.5.411
- 1.5.410
- 1.5.409
- 1.5.408
- 1.5.407
- 1.5.406
- 1.5.405
- 1.5.404
- 1.5.403
- 1.5.402
- 1.5.401
- 1.5.400
- 1.5.399
- 1.5.398
- 1.5.397
- 1.5.396
- 1.5.395
- 1.5.394
- 1.5.393
- 1.5.392
- 1.5.391
- 1.5.390
- 1.5.389
- 1.5.388
- 1.5.387
- 1.5.386
- 1.5.385
- 1.5.384
- 1.5.383
- 1.5.382
- 1.5.381
- 1.5.380
- 1.5.379
- 1.5.378
- 1.5.377
- 1.5.376
- 1.5.375
- 1.5.374
- 1.5.373
- 1.5.372
- 1.5.371
- 1.5.370
- 1.5.369
- 1.5.368
- 1.5.367
- 1.5.366
- 1.5.365
- 1.5.364
- 1.5.363
- 1.5.362
- 1.5.361
- 1.5.360
- 1.5.359
- 1.5.358
- 1.5.357
- 1.5.356
- 1.5.355
- 1.5.354
- 1.5.353
- 1.5.352
- 1.5.351
- 1.5.350
- 1.5.349
- 1.5.345
- 1.5.344
- 1.5.343
- 1.5.342
- 1.5.341
- 1.5.340
- 1.5.339
- 1.5.338
- 1.5.337
- 1.5.336
- 1.5.335
- 1.5.334
- 1.5.333
- 1.5.332
- 1.5.331
- 1.5.330
- 1.5.329
- 1.5.328
- 1.5.327
- 1.5.326
- 1.5.325
- 1.5.324
- 1.5.323
- 1.5.322
- 1.5.321
- 1.5.320
- 1.5.319
- 1.5.318
- 1.5.317
- 1.5.316
- 1.5.315
- 1.5.314
- 1.5.313
- 1.5.312
- 1.5.311
- 1.5.310
- 1.5.309
- 1.5.308
- 1.5.307
- 1.5.306
- 1.5.305
- 1.5.304
- 1.5.303
- 1.5.302
- 1.5.301
- 1.5.300
- 1.5.299
- 1.5.298
- 1.5.297
- 1.5.296
- 1.5.295
- 1.5.294
- 1.5.293
- 1.5.292
- 1.5.291
- 1.5.290
- 1.5.289
- 1.5.288
- 1.5.287
- 1.5.286
- 1.5.285
- 1.5.284
- 1.5.283
- 1.5.282
- 1.5.281
- 1.5.280
- 1.5.279
- 1.5.278
- 1.5.277
- 1.5.276
- 1.5.275
- 1.5.274
- 1.5.273
- 1.5.272
- 1.5.271
- 1.5.270
- 1.5.269
- 1.5.268
- 1.5.267
- 1.5.266
- 1.5.265
- 1.5.264
- 1.5.263
- 1.5.262
- 1.5.261
- 1.5.260
- 1.5.259
- 1.5.258
- 1.5.257
- 1.5.256
- 1.5.255
- 1.5.254
- 1.5.253
- 1.5.252
- 1.5.251
- 1.5.250
- 1.5.249
- 1.5.248
- 1.5.247
- 1.5.246
- 1.5.245
- 1.5.244
- 1.5.243
- 1.5.242
- 1.5.241
- 1.5.240
- 1.5.239
- 1.5.238
- 1.5.237
- 1.5.236
- 1.5.235
- 1.5.234
- 1.5.233
- 1.5.232
- 1.5.231
- 1.5.230
- 1.5.229
- 1.5.228
- 1.5.227
- 1.5.226
- 1.5.225
- 1.5.224
- 1.5.223
- 1.5.222
- 1.5.221
- 1.5.220
- 1.5.219
- 1.5.218
- 1.5.217
- 1.5.216
- 1.5.215
- 1.5.214
- 1.5.213
- 1.5.212
- 1.5.211
- 1.5.210
- 1.5.209
- 1.5.208
- 1.5.207
- 1.5.206
- 1.5.205
- 1.5.204
- 1.5.203
- 1.5.202
- 1.5.201
- 1.5.200
- 1.5.199
- 1.5.198
- 1.5.197
- 1.5.196
- 1.5.195
- 1.5.194
- 1.5.193
- 1.5.192
- 1.5.191
- 1.5.190
- 1.5.189
- 1.5.188
- 1.5.187
- 1.5.186
- 1.5.185
- 1.5.184
- 1.5.183
- 1.5.182
- 1.5.181
- 1.5.180
- 1.5.179
- 1.5.178
- 1.5.177
- 1.5.176
- 1.5.175
- 1.5.174
- 1.5.173
- 1.5.172
- 1.5.171
- 1.5.170
- 1.5.169
- 1.5.168
- 1.5.167
- 1.5.166
- 1.5.165
- 1.5.164
- 1.5.163
- 1.5.162
- 1.5.161
- 1.5.160
- 1.5.159
- 1.5.158
- 1.5.157
- 1.5.156
- 1.5.155
- 1.5.154
- 1.5.153
- 1.5.152
- 1.5.151
- 1.5.150
- 1.5.149
- 1.5.148
- 1.5.147
- 1.5.146
- 1.5.145
- 1.5.144
- 1.5.143
- 1.5.142
- 1.5.141
- 1.5.140
- 1.5.139
- 1.5.138
- 1.5.137
- 1.5.136
- 1.5.135
- 1.5.134
- 1.5.133
- 1.5.132
- 1.5.131
- 1.5.130
- 1.5.129
- 1.5.128
- 1.5.127
- 1.5.126
- 1.5.125
- 1.5.124
- 1.5.123
- 1.5.122
- 1.5.121
- 1.5.120
- 1.5.119
- 1.5.118
- 1.5.117
- 1.5.116
- 1.5.115
- 1.5.114
- 1.5.113
- 1.5.112
- 1.5.111
- 1.5.110
- 1.5.109
- 1.5.108
- 1.5.107
- 1.5.106
- 1.5.105
- 1.5.104
- 1.5.103
- 1.5.102
- 1.5.101
- 1.5.100
- 1.5.99
- 1.5.98
- 1.5.97
- 1.5.96
- 1.5.95
- 1.5.94
- 1.5.93
- 1.5.92
- 1.5.91
- 1.5.90
- 1.5.89
- 1.5.88
- 1.5.87
- 1.5.86
- 1.5.85
- 1.5.84
- 1.5.83
- 1.5.82
- 1.5.81
- 1.5.80
- 1.5.79
- 1.5.78
- 1.5.77
- 1.5.76
- 1.5.75
- 1.5.74
- 1.5.73
- 1.5.72
- 1.5.71
- 1.5.70
- 1.5.69
- 1.5.68
- 1.5.67
- 1.5.66
- 1.5.65
- 1.5.64
- 1.5.63
- 1.5.62
- 1.5.61
- 1.5.60
- 1.5.59
- 1.5.58
- 1.5.57
- 1.5.56
- 1.5.55
- 1.5.54
- 1.5.53
- 1.5.52
- 1.5.51
- 1.5.50
- 1.5.49
- 1.5.48
- 1.5.47
- 1.5.46
- 1.5.45
- 1.5.44
- 1.5.43
- 1.5.42
- 1.5.41
- 1.5.40
- 1.5.39
- 1.5.38
- 1.5.37
- 1.5.36
- 1.5.35
- 1.5.34
- 1.5.33
- 1.5.32
- 1.5.31
- 1.5.30
- 1.5.29
- 1.5.28
- 1.5.27
- 1.5.26
- 1.5.25
- 1.5.24
- 1.5.23
- 1.5.22
- 1.5.21
- 1.5.20
- 1.5.19
- 1.5.18
- 1.5.17
- 1.5.16
- 1.5.15
- 1.5.14
- 1.5.13
- 1.5.12
- 1.5.11
- 1.5.10
- 1.5.9
- 1.5.8
- 1.5.7
- 1.5.6
- 1.5.5
- 1.5.4
- 1.5.3
- 1.5.2
- 1.5.1
- 1.5.0
- 1.4.103
- 1.4.102
- 1.4.101
- 1.4.100
- 1.4.99
- 1.4.98
- 1.4.97
- 1.4.96
- 1.4.95
- 1.4.94
- 1.4.93
- 1.4.92
- 1.4.91
- 1.4.90
- 1.4.89
- 1.4.88
- 1.4.87
- 1.4.86
- 1.4.85
- 1.4.84
- 1.4.83
- 1.4.82
- 1.4.81
- 1.4.80
- 1.4.79
- 1.4.78
- 1.4.77
- 1.4.76
- 1.4.75
- 1.4.74
- 1.4.73
- 1.4.72
- 1.4.71
- 1.4.70
- 1.4.69
- 1.4.68
- 1.4.67
- 1.4.66
- 1.4.65
- 1.4.64
- 1.4.63
- 1.4.62
- 1.4.61
- 1.4.60
- 1.4.59
- 1.4.58
- 1.4.57
- 1.4.56
- 1.4.55
- 1.4.54
- 1.4.53
- 1.4.52
- 1.4.51
- 1.4.50
- 1.4.49
- 1.4.48
- 1.4.44
- 1.4.43
- 1.4.42
- 1.4.41
- 1.4.40
- 1.4.39
- 1.4.38
- 1.4.37
- 1.4.36
- 1.4.35
- 1.4.34
- 1.4.33
- 1.4.32
- 1.4.31
- 1.4.30
- 1.4.29
- 1.4.28
- 1.4.27
- 1.4.26
- 1.4.25
- 1.4.24
- 1.4.23
- 1.4.22
- 1.4.21
- 1.4.20
- 1.4.19
- 1.4.18
- 1.4.17
- 1.4.16
- 1.4.15
- 1.4.14
- 1.4.13
- 1.4.12
- 1.4.11
- 1.4.10
- 1.4.9
- 1.4.8
- 1.4.7
- 1.4.6
- 1.4.5
- 1.4.4
- 1.4.3
- 1.4.2
- 1.4.1
- 1.4.0
- 1.3.14
- 1.3.13
- 1.3.12
- 1.3.11
- 1.3.10
- 1.3.9
- 1.3.8
- 1.3.7
- 1.3.6
- 1.3.5
- 1.3.4
- 1.2.56
- 1.2.55
- 1.2.54
- 1.2.53
- 1.2.52
- 1.2.51
- 1.2.50
- 1.2.49
- 1.2.48
- 1.2.47
- 1.2.46
- 1.2.45
- 1.2.44
- 1.2.43
- 1.2.42
- 1.2.41
- 1.2.40
- 1.2.39
- 1.2.38
- 1.2.37
- 1.2.36
- 1.2.34
- 1.2.33
- 1.2.32
- 1.2.31
- 1.2.30
- 1.2.29
- 1.2.28
- 1.2.27
- 1.2.26
- 1.2.25
- 1.2.24
- 1.2.23
- 1.2.22
- 1.2.21
This package is auto-updated.
Last update: 2026-10-01 17:15:46 UTC
README
Run, background, and monitor Symfony console commands. Four things:
- Web command runner — run any
#[AsCommand]from a web page (with the Symfony profiler available), for easier debugging. - Background runner —
bg:run "<cmd>"dispatches a command to an always-on Messenger worker so long jobs survive logout / container teardown. - Process registry + monitor — every background run is recorded as a
CommandProcess(status, timing, output, named "slots"); watch them live in a TUI (bg:monitor) or the web list (/…/processes). - Agent tools (MCP) — opted-in commands become MCP tools on symfony/mcp-bundle's server, with the schema derived from the command's own
#[Argument]/#[Option]attributes. See Agent tools.
Requirements
- PHP 8.4+, Symfony 8.1+
- Doctrine ORM (ships the
CommandProcessentity) symfony/messenger(forbg:runand background recording)survos/tui-extras-bundle— optional, only for thebg:monitorTUI (require-dev/suggest)
Commands
| Command | What |
|---|---|
bg:run "<cmd>" |
Dispatch <cmd> to the command Messenger worker. One message per command (fan out). |
bg:monitor (alias monitor) |
Live TUI of background runs, grouped by command, status by glyph, most-recent-first. Needs survos/tui-extras-bundle. |
bg:run requires the app to route RunCommandMessage to an async transport and run a worker:
# config/packages/messenger.yaml framework: messenger: transports: command: 'doctrine://default?queue_name=command' routing: 'Symfony\Component\Console\Messenger\RunCommandMessage': command
php bin/console messenger:consume command -vv # local worker (normal verbosity — see note) dokku ps:scale <app> command=1 # prod worker
Verbosity note: captured output inherits the worker's verbosity.
messenger:consume -qmakes the run's output QUIET (nothing captured); run at normal verbosity to record output.
Process registry & slots
Background runs are recorded in the command_process table (only bg:run runs — plain CLI/web runs are not recorded). A command can push a styled, named status fragment to the monitor with plain PSR-3 logging:
$logger->info($institution, ['tui.slot' => 'header']); // → process.slots['header'], shown live
Configuration
survos_command: routes_enabled: false # OFF by default — these routes RUN console commands (footgun) route_prefix: /admin/commands # keep behind a secured prefix base_layout: ~ # app layout for the web pages (must load importmap/Stimulus) track: true # record bg runs as CommandProcess + enable the tui.slot handler namespaces: [] # web UI: only list commands in these namespaces ([] = all)
Upgrading (process registry)
This bundle now ships the CommandProcess Doctrine entity and requires doctrine/orm, survos/field-bundle, symfony/messenger, and symfony/uid. After updating, create the table:
- SQLite (dev):
bin/console doctrine:schema:update --force - PostgreSQL / shared:
bin/console doctrine:migrations:diff→ review → migrate
Routes default to off — set survos_command.routes_enabled: true (under a secured prefix) to use the web UI/monitor.
Agent tools (MCP)
Agents run commands you'd otherwise run over SSH. With symfony/mcp-bundle installed, explicitly opted-in commands become tools on its MCP server. Nothing else is exposed.
Why this exists, next to symfony/mcp-bundle
symfony/mcp-bundle is the MCP server: transports (HTTP, stdio), sessions, the #[McpTool] attribute,
debug:mcp, the profiler panel. This bundle does not replace any of it; it adds what that server
does not have: a tool that is a console command.
#[McpTool] (symfony/mcp-bundle) |
#[AsAgentTool] (this bundle) |
|
|---|---|---|
| What you write | a method for the agent, separate from any command | nothing extra: one attribute on the command |
| Inputs declared | in the method signature, again | once, on the command (#[Argument]/#[Option], #[MapInput] DTOs) |
| Grouped inputs (a DTO) | not supported by the SDK's schema generator | yes, via #[MapInput] |
| Run it yourself | no CLI equivalent | bin/console <command> --format=json is the same call |
Vendor commands (messenger:stats) |
would need a wrapper | one line of config |
| Access control | none built in; an open /mcp exposes every tool |
ROLE_ADMIN per tool unless marked public |
| Record of calls | profiler, in dev | CommandProcess rows: caller, command line, status (agent:calls) |
| Long-running work | blocks the request | the same command can go through bg:run |
Use #[McpTool] for things that are not commands (a search over an index, a resource). Use
#[AsAgentTool] for anything you would otherwise ask an agent to run over SSH.
Opting a command in
Your own command: #[AsAgentTool] next to #[AsCommand]:
#[AsCommand('app:add-page', 'Add or update a headlines page to scan')] #[AsAgentTool('add_page', idempotent: true, description: <<<'TEXT' Add (or update) a listing page that gets scanned for headlines. Creates the media (website) from the URL's host when it is new; an existing page URL is updated, not duplicated. Tags go on the media; get their ids from find_tags. Call with dry-run first to see what would change. TEXT)] public function addPage(SymfonyStyle $io, #[MapInput] PageInput $page, #[Option('Output format: text or json')] string $format = 'text'): int
A command you can't annotate (vendor or another bundle's): config.
survos_command: agent_tools: - { command: 'messenger:stats', readOnly: true, public: true } # tool "messenger_stats", open - { command: 'app:purge', destructive: true } # needs ROLE_ADMIN - command: 'state:stats' readOnly: true description: 'Where things are in a workflow: counts per marking, …'
A bundle should not decide for an app what is exposed, so bundle commands (like state-bundle's
state:stats) carry no attribute: they provide --format=json, and each app opts in by config.
What the agent is told about a tool
Agents discover tools from the server and choose by what each tool says about itself, so say it well:
| Field | Comes from |
|---|---|
| name | AsAgentTool::$name, or the command name (app:add-page → app_add_page) |
| title | AsAgentTool::$title, or the name humanised ("Add page") |
| description | AsAgentTool::$description (config: description), else the command's one-line description. Write it for an agent: what it does, when to use it, what comes back, in terms of parameter names. The command's CLI help is never sent: it is written for a terminal (php bin/console …, --flags). |
| parameters | the command's InputDefinition: names, descriptions, required arguments, defaults |
| parameter types | the PHP signature: int → integer, float → number, flags → boolean, repeatable options → array, BackedEnum → enum of its values. Config-listed commands only have what the console knows (strings, flags, arrays). |
| read-only / destructive / idempotent hints | AsAgentTool (config: readOnly, destructive, idempotent); clients use them to decide what needs confirmation |
Server-wide guidance (how the tools fit together) goes in symfony/mcp-bundle's mcp.servers.<name>.instructions.
Check the result with bin/console debug:mcp and bin/console debug:mcp <tool>.
How a call runs
- The command runs in-process (
ConsoleCommandExecutor), with--format=jsonwhen it has a--formatoption. The decoded JSON is the tool result; other output comes back as{output: "…"}.--format=jsonis also how you debug the same call from the CLI: same command, different caller. - Errors (exceptions, non-zero exit, unknown parameters) come back to the agent as readable tool errors.
Security
Access is decided per tool, closed by default: every tool requires ROLE_ADMIN (or the role: it names), checked on every call. That is for HTTP callers: over stdio the server is a local process started by someone who already has a shell on the machine, so roles are not checked there and the call is recorded as stdio:<user>. A tool that exposes nothing sensitive opts out with public: true, and only a readOnly tool may (enforced at container compile):
#[AsAgentTool(readOnly: true, public: true)] // tags, media, queue counts: open #[AsAgentTool(readOnly: true)] // a list of users: still ROLE_ADMIN #[AsAgentTool(idempotent: true)] // a write: ROLE_ADMIN
The check fails closed: without symfony/security-bundle, a non-public tool is denied. Plain #[McpTool] methods are not gated by the bridge, and with an open /mcp they're open to everyone, so on such a server write lookups as commands with #[AsAgentTool] too: one gate for every tool, and each one is a CLI command you can debug.
Signing in: a stateless access_token firewall on /mcp. AgentTokenHandler signs the bearer token in as a real user from your provider, so calls carry that user's roles. The endpoint itself stays open, so read tools work without a token:
# config/packages/security.yaml firewalls: mcp: pattern: ^/mcp stateless: true provider: app_user_provider access_token: token_handler: Survos\CommandBundle\Security\AgentTokenHandler # main: … access_control: - { path: ^/mcp, roles: PUBLIC_ACCESS } # each tool gates itself
# config/packages/survos_command.yaml survos_command: agent: token: '%env(default::AGENT_TOKEN)%' # unset = no token accepted user: 'admin@example.com'
Connect: claude mcp add --transport http myapp https://myapp.example/mcp --header "Authorization: Bearer $AGENT_TOKEN"
Seeing what agents can do, and did
bin/console debug:mcp(symfony/mcp-bundle) lists every tool on the server;debug:mcp <tool>shows its description and input schema.bin/console agent:callslists the calls agents made: caller, status, and the equivalent command line so any call can be re-run by hand. Options:--command,--caller,--failed,--output,--limit,--format=json. It is itself an admin-only tool (agent_calls).
With survos_command.track on (the default), every tool call is a CommandProcess row in mode agent, refused calls included (failed, "Access denied…", caller anonymous when no token was sent). After upgrading, add the new column: command_process.caller (doctrine:migrations:diff).
OAuth (for claude.ai web/mobile connectors) is not wired: mcp/sdk ships resource-server middleware (JWT validation, protected-resource metadata), but it needs an identity provider to issue the tokens.
Web command runner
Long-running commands: see symfony/symfony#59696. The run form also has a "Dispatch via Messenger (async)" checkbox.
Purpose
Use assert(), dump() and dd() are quick and easy debug tools when debugging a Symfony web page. But it's often difficult to use within the console, since the formatting is for a web page.
For example, in the official Symfony Demo, there is a command to send the list of users to an email address.
bin/console app:list-users --send-to=admin@example.com
Debugging this is much easier with Symfony's Debug Toolbar, this bundle wraps the console commands with a web interface so that the toolbar is available.
symfony local:new --demo --dir=symfony-demo
cd symfony-demo
composer require survos/command-bundle
Now go to /admin/commands and see what's available
Select list-users, and fill in the email.
Submit the form and open the debug toolbar:
With dumps and asserts, this is even more helpful.
Example with Symfony Demo
composer config extra.symfony.allow-contrib true composer config extra.symfony.endpoint --json '["https://raw.githubusercontent.com/symfony/recipes-contrib/flex/pull-1708/index.json", "flex://defaults"]'
composer create-project symfony/symfony-demo command-demo cd command-demo sed -i 's/"php": "8.2.0"//' composer.json composer config extra.symfony.allow-contrib true composer req survos/command-bundle bin/console --version symfony server:start -d symfony open:local --path admin/commands
Defining commands
Symfony 8.1+ lets you mark methods on any service with #[AsCommand], with arguments and options described via attributes. The web form rendered by this bundle introspects the same metadata the CLI uses, so no separate command class is required.
namespace App\Command; use App\Repository\PostRepository; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Style\SymfonyStyle; final class PostCommands { public function __construct(private PostRepository $posts) {} #[AsCommand('app:list-posts', 'List the posts')] public function list( SymfonyStyle $io, #[Option(description: 'Limit the number of posts')] int $limit = 50, ): void { $rows = array_map( fn ($p) => [$p->getId(), $p->getTitle(), $p->getAuthor()->getFullName()], $this->posts->findBy([], [], $limit), ); $io->table(['id', 'title', 'author'], $rows); } }
The same form supports a "Dispatch via Messenger (async)" checkbox for long-running work — wire up Messenger and the command runs in a worker rather than the request cycle.
with castor
symfony new castor-command-demo --webapp && cd castor-command-demo sed -i "s|# MAILER_DSN|MAILER_DSN|" .env bin/console make:command app:castor-test cat > castor.php <<'END' <?php use Castor\Attribute\AsTask; use function Castor\io; use function Castor\capture; use function Castor\import; import(__DIR__ . '/src/Command/CastorTestCommand.php'); #[AsTask(description: 'Welcome to Castor!')] function hello(): void { $currentUser = capture('whoami'); io()->title(sprintf('Hello %s!', $currentUser)); } END
Add attribute to AppCastorTest.php
#[\Castor\Attribute\AsSymfonyTask()]
castor list


