You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(mcp): harden and modernize the MCP server layer (#738)
* feat(mcp): harden and modernize the MCP server layer
- Make operation tools stateless per call: tools hold only the repository
class string and resolve a fresh Repository in every handle(), fixing the
root cause behind #735 (singleton-cached repository state) and removing
the store-model band-aid. Collapse the 8 duplicate execute* dispatch
methods in McpToolsManager.
- Return real MCP errors: all failure paths now set isError=true via
Response::error() with a stable {error, code, errors, detail} payload;
actions/getters propagate per-field ValidationException detail like CRUD;
raw exception messages only surface when app.debug is on; fix the
'getter' vs 'action' key in the action authorization error.
- Add tool annotations (#[IsReadOnly]/#[IsDestructive]/#[IsIdempotent]/
#[IsOpenWorld]) to every operation, wrapper, and search tool, and expose
the same hints per operation in the wrapper catalog payloads.
- Harden the server: default mode flips to 'wrapper' (breaking, 10.x),
the per-request ?mode= override is removed, new restify.mcp.read_only
mode gates write operations in discovery and execution, and store file
ingestion rejects local paths by default, blocks private-IP URLs, and
caps download size.
- Emit structuredContent alongside JSON text on tool results so hosts can
parse responses deterministically.
- New restify:skill artisan command generating SKILL.md + openapi.json
from MCP-enabled repositories (auth, pagination, filter conventions,
per-repository operations and curl examples).
BREAKING CHANGE: restify.mcp.mode now defaults to 'wrapper'; set
RESTIFY_MCP_MODE=direct to keep the previous per-operation tool exposure.
* Fix styling
* fix(tests): use a temp file instead of /etc/hosts in the local-path guard test
/etc/hosts does not exist on Windows runners, so the allow_local_paths
assertion received null instead of an UploadedFile. A test-created temp
file also proves the default rejection against a genuinely readable file.
---------
Co-authored-by: binaryk <6833714+binaryk@users.noreply.github.com>
- Override automatic type inference when it's incorrect
170
170
171
+
### Error Responses
172
+
173
+
When an operation fails, Restify returns a real MCP error result with `isError` set to `true` on the tool result, so agents can reliably distinguish failures from successful calls.
174
+
175
+
The error content is a JSON payload with a stable shape. The keys are guaranteed to be stable so agents can parse them:
176
+
177
+
```json
178
+
{
179
+
"error": "Validation failed",
180
+
"code": "VALIDATION_ERROR",
181
+
"errors": {
182
+
"title": ["The title field is required."]
183
+
}
184
+
}
185
+
```
186
+
187
+
-`error`: a human-readable message.
188
+
-`code`: a machine-readable code such as `MISSING_PARAMETER`, `INVALID_OPERATION`, `VALIDATION_ERROR`, `AUTHORIZATION_ERROR`, `NOT_FOUND`, or `EXECUTION_ERROR`.
189
+
-`errors`: present for validation failures, containing per-field messages. Actions and getters surface the same per-field detail as CRUD `store`/`update` operations.
190
+
-`detail`: present only for unexpected server errors and only when `app.debug` is enabled. Internal exception messages are never leaked to clients in production.
191
+
192
+
### Structured Tool Output
193
+
194
+
Successful tool results are emitted with both the existing JSON `text` content and a machine-parsable `structuredContent` field on the same tool result. This lets hosts read the payload deterministically without re-parsing the text block, while clients that only read `text` continue to work unchanged.
195
+
196
+
Both the operation tools (index, show, store, update, delete, profile, action, getter) and the 4 wrapper tools (`discover-repositories`, `get-repository-operations`, `get-operation-details`, `execute-operation`) return structured content. The `structuredContent` value is always byte-for-byte identical to the JSON parsed from the `text` block:
The three read-only wrapper discovery tools also publish an `outputSchema` (their shape is stable), so hosts can validate `structuredContent` up front. Operation tools such as index/show derive their shape from repository fields at runtime and therefore do not declare a static `outputSchema`.
211
+
212
+
Error results are intentionally **not** given `structuredContent`: they are already flagged with `isError: true` and carry the stable JSON error payload described above in their `text` content.
213
+
171
214
## Configuration
172
215
173
216
The MCP integration respects your existing Restify configuration and adds MCP-specific options:
@@ -178,7 +221,13 @@ The MCP integration respects your existing Restify configuration and adds MCP-sp
178
221
'server_name' => 'My App MCP Server',
179
222
'server_version' => '1.0.0',
180
223
'default_pagination' => 25,
181
-
'mode' => env('RESTIFY_MCP_MODE', 'direct'), // 'direct' or 'wrapper'
224
+
'mode' => env('RESTIFY_MCP_MODE', 'wrapper'), // 'wrapper' (default) or 'direct'
225
+
'read_only' => env('RESTIFY_MCP_READ_ONLY', false), // refuse all write operations
@@ -190,11 +239,15 @@ The MCP integration respects your existing Restify configuration and adds MCP-sp
190
239
],
191
240
```
192
241
242
+
::alert{type="warning"}
243
+
As of Restify 10, the MCP mode defaults to **`wrapper`** (previously `direct`). Wrapper mode exposes only 4 tools and hides individual operations behind a progressive discovery workflow. Set `RESTIFY_MCP_MODE=direct` to restore the previous behavior.
244
+
::
245
+
193
246
## MCP Mode: Direct vs Wrapper
194
247
195
248
Laravel Restify offers two modes for exposing your repositories through MCP: **Direct Mode** and **Wrapper Mode**. Each mode has different trade-offs in terms of token usage and discoverability.
196
249
197
-
### Direct Mode (Default)
250
+
### Direct Mode
198
251
199
252
In direct mode, every repository operation (index, show, store, update, delete) and custom action/getter is exposed as a separate MCP tool. This provides maximum discoverability for AI agents.
200
253
@@ -212,9 +265,9 @@ In direct mode, every repository operation (index, show, store, update, delete)
212
265
RESTIFY_MCP_MODE=direct
213
266
```
214
267
215
-
### Wrapper Mode (Token-Efficient)
268
+
### Wrapper Mode (Default, Token-Efficient)
216
269
217
-
Wrapper mode uses a progressive discovery pattern that exposes only **4 wrapper tools** regardless of how many repositories you have. AI agents discover and execute operations through a multi-step workflow.
270
+
Wrapper mode is the default. It uses a progressive discovery pattern that exposes only **4 wrapper tools** regardless of how many repositories you have. AI agents discover and execute operations through a multi-step workflow.
218
271
219
272
**When to use Wrapper Mode:**
220
273
- You have many repositories (10+)
@@ -279,24 +332,43 @@ Lists all operations, actions, and getters available for a specific repository.
Each operation, action, and getter carries an `annotations` object with the same [MCP tool safety hints](#tool-safety-annotations) that direct mode exposes on the tool definitions, so agents get consistent safety signals through the wrapper path.
371
+
300
372
#### 3. `get-operation-details`
301
373
Returns the complete JSON schema and documentation for a specific operation, including all parameters, validation rules, and examples.
302
374
@@ -316,6 +388,7 @@ Returns the complete JSON schema and documentation for a specific operation, inc
316
388
"type": "create",
317
389
"title": "Create User",
318
390
"description": "Create a new user account",
391
+
"annotations": {},
319
392
"schema": {
320
393
"type": "object",
321
394
"properties": {
@@ -367,6 +440,20 @@ Executes a repository operation with the provided parameters. This is the final
367
440
}
368
441
```
369
442
443
+
### Tool Safety Annotations
444
+
445
+
Every operation tool is tagged with the [MCP tool annotations](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations) that describe its behavior, so a host can warn before running a destructive call or safely cache a read-only one. Restify applies them automatically based on the operation type:
In **direct mode** these appear in the `annotations` field of each tool definition returned by `tools/list`. In **wrapper mode** the per-operation tools are not registered with the host, so the same hints are surfaced inside the `annotations` object of every entry returned by `get-repository-operations` and `get-operation-details`.
456
+
370
457
### Wrapper Mode Workflow
371
458
372
459
Here's a typical workflow when an AI agent uses wrapper mode:
@@ -383,14 +470,14 @@ This progressive discovery pattern reduces token usage while maintaining full fu
383
470
You can switch between modes at any time by updating your `.env` file:
384
471
385
472
```bash
386
-
# Direct mode (default)
387
-
RESTIFY_MCP_MODE=direct
388
-
389
-
# Wrapper mode (token-efficient)
473
+
# Wrapper mode (default, token-efficient)
390
474
RESTIFY_MCP_MODE=wrapper
475
+
476
+
# Direct mode
477
+
RESTIFY_MCP_MODE=direct
391
478
```
392
479
393
-
No code changes are required. The MCP server automatically adapts to the configured mode.
480
+
No code changes are required. The MCP server automatically adapts to the configured mode. The mode is read from configuration only — a request parameter cannot override it.
394
481
395
482
### Performance Considerations
396
483
@@ -408,12 +495,66 @@ No code changes are required. The MCP server automatically adapts to the configu
408
495
409
496
### Best Practices
410
497
411
-
1.**Start with Direct Mode**during development to verify all tools are working correctly
412
-
2.**Switch to Wrapper Mode**in production if you have 10+ repositories or token efficiency is important
498
+
1.**Keep the default Wrapper Mode**in production if you have 10+ repositories or token efficiency is important
499
+
2.**Switch to Direct Mode**during development when you want every tool visible immediately
413
500
3.**Use wrapper mode** when working with AI agents that have limited context windows
414
501
4.**Monitor token usage** to determine which mode is best for your application
415
502
5.**Document your choice** so team members understand which mode is active
416
503
504
+
## Read-Only Mode
505
+
506
+
When you want to give an AI agent access to your data without letting it mutate anything, enable read-only mode:
507
+
508
+
```bash
509
+
RESTIFY_MCP_READ_ONLY=true
510
+
```
511
+
512
+
With read-only mode enabled, every write operation (`store`, `update`, `delete`, and custom actions) is refused:
513
+
514
+
- In **direct mode**, only read tools (`index`, `show`, getters, and `profile`) are registered with the host. Write tools are never exposed.
515
+
- In **wrapper mode**, write operations are hidden from `discover-repositories`, `get-repository-operations`, and `get-operation-details`, and `execute-operation` rejects them with an authorization-style `isError` response.
516
+
517
+
Enforcement happens centrally, so an agent cannot re-enable a hidden operation by calling it directly.
518
+
519
+
## Generating an Agent Skill
520
+
521
+
Agents increasingly consume APIs through [Agent Skills](https://www.anthropic.com/news/skills) and code execution rather than raw MCP calls. Restify can emit a ready-to-ship skill straight from your MCP-enabled repositories, so the documentation an agent reads never drifts from what your API actually exposes:
522
+
523
+
```bash
524
+
php artisan restify:skill
525
+
```
526
+
527
+
By default the files are written to `./restify-skill/`. Pass `--path` to choose another directory:
-**`SKILL.md`** — a Claude Agent Skill with YAML front matter and markdown that teaches the base URL convention (`/api/restify/{repository}`), Sanctum bearer authentication, pagination, sorting, related resources, search, and Restify's filter convention (`filterName=value`, **not**`filter[name]=value`). It then documents one section per MCP-enabled repository: available operations, the field schema, matchables/sortables/searchables, and copy-paste `curl` examples.
536
+
-**`openapi.json`** — an OpenAPI 3.1 description with a path per enabled operation (`index`, `show`, `store`, `update`, `delete`, actions, and getters), request bodies derived from your store/update validation rules, and a `bearerAuth` security scheme.
537
+
538
+
Only repositories using the `HasMcpTools` trait and the operations they expose are included. [Read-only mode](#read-only-mode) is honored too: when it's enabled, write operations and actions are omitted from both files.
539
+
540
+
## File Ingestion Safety
541
+
542
+
Repository `File` fields accept an AI-supplied value that Restify turns into an upload. To avoid server-side request forgery (SSRF) and local file disclosure, ingestion is locked down by default:
543
+
544
+
-**Local filesystem paths are disabled.** A value like `/etc/hosts` is ignored unless you explicitly opt in with `RESTIFY_MCP_FILES_ALLOW_LOCAL_PATHS=true`.
545
+
-**Only `http`/`https` URLs are fetched.** Other schemes (`file://`, `ftp://`, …) are rejected.
546
+
-**Private, loopback, link-local, and reserved IPs are rejected**, including hosts that resolve to them, so an agent cannot reach internal services (for example `http://169.254.169.254` or `http://127.0.0.1`).
547
+
-**Downloads are capped and time-limited** by `RESTIFY_MCP_FILES_MAX_BYTES` (default 10MB) and `RESTIFY_MCP_FILES_TIMEOUT` (default 10 seconds). Redirects are not followed.
548
+
549
+
```bash
550
+
# Opt in to local paths (use with care)
551
+
RESTIFY_MCP_FILES_ALLOW_LOCAL_PATHS=true
552
+
553
+
# Tune the remote fetch guardrails
554
+
RESTIFY_MCP_FILES_MAX_BYTES=10485760
555
+
RESTIFY_MCP_FILES_TIMEOUT=10
556
+
```
557
+
417
558
## Fine-Grained Tool Permissions
418
559
419
560
Laravel Restify's MCP integration includes a powerful permission system that allows you to control which tools each API token can access. This is essential for multi-tenant applications or when you need to restrict AI agent capabilities.
0 commit comments