Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion config/restify.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,47 @@
| regardless of this setting.
|
*/
'mode' => env('RESTIFY_MCP_MODE', 'direct'),
'mode' => env('RESTIFY_MCP_MODE', 'wrapper'),

/*
|--------------------------------------------------------------------------
| Read-Only Mode
|--------------------------------------------------------------------------
|
| When enabled, the MCP server refuses every write operation (store,
| update, delete, action). In 'direct' mode only read tools (index, show,
| getter, profile) are registered; in 'wrapper' mode write operations are
| hidden from discovery and rejected by execute-operation.
|
*/
'read_only' => env('RESTIFY_MCP_READ_ONLY', false),

/*
|--------------------------------------------------------------------------
| File Ingestion
|--------------------------------------------------------------------------
|
| Controls how AI-supplied file fields are resolved into uploads.
|
*/
'files' => [
/*
| Allow AI-supplied values to reference local filesystem paths. Disabled
| by default: enabling it lets an MCP client read arbitrary server files.
*/
'allow_local_paths' => env('RESTIFY_MCP_FILES_ALLOW_LOCAL_PATHS', false),

/*
| Maximum number of bytes fetched from a remote file URL. Downloads
| larger than this are rejected. Defaults to 10MB.
*/
'max_bytes' => env('RESTIFY_MCP_FILES_MAX_BYTES', 10 * 1024 * 1024),

/*
| Timeout in seconds applied when fetching a remote file URL.
*/
'timeout' => env('RESTIFY_MCP_FILES_TIMEOUT', 10),
],

'tools' => [
'exclude' => [
Expand Down
169 changes: 155 additions & 14 deletions docs-v3/content/docs/6.mcp/1.mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,49 @@ This approach allows you to:
- Set validation rules (required, optional)
- Override automatic type inference when it's incorrect

### Error Responses

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.

The error content is a JSON payload with a stable shape. The keys are guaranteed to be stable so agents can parse them:

```json
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"errors": {
"title": ["The title field is required."]
}
}
```

- `error`: a human-readable message.
- `code`: a machine-readable code such as `MISSING_PARAMETER`, `INVALID_OPERATION`, `VALIDATION_ERROR`, `AUTHORIZATION_ERROR`, `NOT_FOUND`, or `EXECUTION_ERROR`.
- `errors`: present for validation failures, containing per-field messages. Actions and getters surface the same per-field detail as CRUD `store`/`update` operations.
- `detail`: present only for unexpected server errors and only when `app.debug` is enabled. Internal exception messages are never leaked to clients in production.

### Structured Tool Output

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.

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:

```json
{
"result": {
"content": [
{ "type": "text", "text": "{ \"success\": true, \"total\": 1, ... }" }
],
"structuredContent": { "success": true, "total": 1, "...": "..." },
"isError": false
}
}
```

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`.

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.

## Configuration

The MCP integration respects your existing Restify configuration and adds MCP-specific options:
Expand All @@ -178,7 +221,13 @@ The MCP integration respects your existing Restify configuration and adds MCP-sp
'server_name' => 'My App MCP Server',
'server_version' => '1.0.0',
'default_pagination' => 25,
'mode' => env('RESTIFY_MCP_MODE', 'direct'), // 'direct' or 'wrapper'
'mode' => env('RESTIFY_MCP_MODE', 'wrapper'), // 'wrapper' (default) or 'direct'
'read_only' => env('RESTIFY_MCP_READ_ONLY', false), // refuse all write operations
'files' => [
'allow_local_paths' => env('RESTIFY_MCP_FILES_ALLOW_LOCAL_PATHS', false),
'max_bytes' => env('RESTIFY_MCP_FILES_MAX_BYTES', 10 * 1024 * 1024),
'timeout' => env('RESTIFY_MCP_FILES_TIMEOUT', 10),
],
'tools' => [
'exclude' => [
// Tools to exclude from discovery
Expand All @@ -190,11 +239,15 @@ The MCP integration respects your existing Restify configuration and adds MCP-sp
],
```

::alert{type="warning"}
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.
::

## MCP Mode: Direct vs Wrapper

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.

### Direct Mode (Default)
### Direct Mode

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.

Expand All @@ -212,9 +265,9 @@ In direct mode, every repository operation (index, show, store, update, delete)
RESTIFY_MCP_MODE=direct
```

### Wrapper Mode (Token-Efficient)
### Wrapper Mode (Default, Token-Efficient)

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.
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.

**When to use Wrapper Mode:**
- You have many repositories (10+)
Expand Down Expand Up @@ -279,24 +332,43 @@ Lists all operations, actions, and getters available for a specific repository.
{
"success": true,
"repository": "users",
"crud_operations": ["index", "show", "store", "update", "delete", "profile"],
"operations": [
{
"type": "index",
"name": "users-index-tool",
"title": "Users",
"description": "List User records",
"annotations": { "readOnlyHint": true }
},
{
"type": "delete",
"name": "users-delete-tool",
"title": "Users Delete",
"description": "Delete an existing User record",
"annotations": { "destructiveHint": true, "idempotentHint": true }
}
],
"actions": [
{
"name": "activate-user",
"title": "Activate User",
"description": "Activate a user account"
"description": "Activate a user account",
"annotations": { "openWorldHint": true }
}
],
"getters": [
{
"name": "active-users",
"title": "Active Users",
"description": "Get all active users"
"description": "Get all active users",
"annotations": { "readOnlyHint": true }
}
]
}
```

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.

#### 3. `get-operation-details`
Returns the complete JSON schema and documentation for a specific operation, including all parameters, validation rules, and examples.

Expand All @@ -316,6 +388,7 @@ Returns the complete JSON schema and documentation for a specific operation, inc
"type": "create",
"title": "Create User",
"description": "Create a new user account",
"annotations": {},
"schema": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -367,6 +440,20 @@ Executes a repository operation with the provided parameters. This is the final
}
```

### Tool Safety Annotations

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:

| Operation | Annotation hints |
| --- | --- |
| `index`, `show`, `profile`, getters, `global-search` | `readOnlyHint: true` |
| `store` | *(none — a create is neither idempotent nor read-only)* |
| `update` | `idempotentHint: true` |
| `delete` | `destructiveHint: true`, `idempotentHint: true` |
| actions | `openWorldHint: true` (an action may touch external systems) |

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`.

### Wrapper Mode Workflow

Here's a typical workflow when an AI agent uses wrapper mode:
Expand All @@ -383,14 +470,14 @@ This progressive discovery pattern reduces token usage while maintaining full fu
You can switch between modes at any time by updating your `.env` file:

```bash
# Direct mode (default)
RESTIFY_MCP_MODE=direct

# Wrapper mode (token-efficient)
# Wrapper mode (default, token-efficient)
RESTIFY_MCP_MODE=wrapper

# Direct mode
RESTIFY_MCP_MODE=direct
```

No code changes are required. The MCP server automatically adapts to the configured mode.
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.

### Performance Considerations

Expand All @@ -408,12 +495,66 @@ No code changes are required. The MCP server automatically adapts to the configu

### Best Practices

1. **Start with Direct Mode** during development to verify all tools are working correctly
2. **Switch to Wrapper Mode** in production if you have 10+ repositories or token efficiency is important
1. **Keep the default Wrapper Mode** in production if you have 10+ repositories or token efficiency is important
2. **Switch to Direct Mode** during development when you want every tool visible immediately
3. **Use wrapper mode** when working with AI agents that have limited context windows
4. **Monitor token usage** to determine which mode is best for your application
5. **Document your choice** so team members understand which mode is active

## Read-Only Mode

When you want to give an AI agent access to your data without letting it mutate anything, enable read-only mode:

```bash
RESTIFY_MCP_READ_ONLY=true
```

With read-only mode enabled, every write operation (`store`, `update`, `delete`, and custom actions) is refused:

- In **direct mode**, only read tools (`index`, `show`, getters, and `profile`) are registered with the host. Write tools are never exposed.
- 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.

Enforcement happens centrally, so an agent cannot re-enable a hidden operation by calling it directly.

## Generating an Agent Skill

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:

```bash
php artisan restify:skill
```

By default the files are written to `./restify-skill/`. Pass `--path` to choose another directory:

```bash
php artisan restify:skill --path=resources/skills/api
```

The command produces two files:

- **`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.
- **`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.

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.

## File Ingestion Safety

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:

- **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`.
- **Only `http`/`https` URLs are fetched.** Other schemes (`file://`, `ftp://`, …) are rejected.
- **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`).
- **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.

```bash
# Opt in to local paths (use with care)
RESTIFY_MCP_FILES_ALLOW_LOCAL_PATHS=true

# Tune the remote fetch guardrails
RESTIFY_MCP_FILES_MAX_BYTES=10485760
RESTIFY_MCP_FILES_TIMEOUT=10
```

## Fine-Grained Tool Permissions

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.
Expand Down
49 changes: 49 additions & 0 deletions src/Commands/SkillCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Binaryk\LaravelRestify\Commands;

use Binaryk\LaravelRestify\MCP\Skill\RestifySkillGenerator;
use Binaryk\LaravelRestify\Restify;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;

class SkillCommand extends Command
{
protected $signature = 'restify:skill {--path= : Directory where the skill files are written (default ./restify-skill)}';

protected $description = 'Generate an Agent Skill (SKILL.md) and OpenAPI description from the MCP-enabled repositories';

public function handle(RestifySkillGenerator $generator): int
{
Restify::ensureRepositoriesLoaded();

$repositories = $generator->repositories();

if ($repositories->isEmpty()) {
$this->warn('No MCP-enabled repositories found. Add the HasMcpTools trait to a repository first.');

return self::SUCCESS;
}

$path = rtrim($this->option('path') ?: getcwd().'/restify-skill', '/');

File::ensureDirectoryExists($path);

$this->info("Generating skill for {$repositories->count()} repository(ies)...");

$repositories->each(function (array $repository): void {
$this->line(" - {$repository['name']}");
});

$skillPath = $path.'/SKILL.md';
$openApiPath = $path.'/openapi.json';

File::put($skillPath, $generator->markdown());
File::put($openApiPath, json_encode($generator->openApi(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES).PHP_EOL);

$this->comment("Wrote {$skillPath}");
$this->comment("Wrote {$openApiPath}");

return self::SUCCESS;
}
}
2 changes: 2 additions & 0 deletions src/LaravelRestifyServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Binaryk\LaravelRestify\Commands\RestifyRouteListCommand;
use Binaryk\LaravelRestify\Commands\SetupAuthCommand;
use Binaryk\LaravelRestify\Commands\SetupCommand;
use Binaryk\LaravelRestify\Commands\SkillCommand;
use Binaryk\LaravelRestify\Commands\StoreCommand;
use Binaryk\LaravelRestify\Commands\StubCommand;
use Binaryk\LaravelRestify\Commands\ToolCommand;
Expand Down Expand Up @@ -54,6 +55,7 @@ public function configurePackage(Package $package): void
ToolCommand::class,
McpResourceCommand::class,
GraphqlGenerateCommand::class,
SkillCommand::class,
]);
}

Expand Down
Loading