Skip to content

feat(mcp): harden and modernize the MCP server layer#738

Merged
binaryk merged 3 commits into
10.xfrom
feat/mcp-improvements
Jul 8, 2026
Merged

feat(mcp): harden and modernize the MCP server layer#738
binaryk merged 3 commits into
10.xfrom
feat/mcp-improvements

Conversation

@binaryk

@binaryk binaryk commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the improvements from a full research + adversarial-verification pass over the MCP layer (architecture review, security audit, MCP 2025-06/2025-11 spec ecosystem research). Six changes, each independently verified:

  1. Stateless operation tools — tools hold only the repository class string and resolve a fresh Repository per handle() call. Fixes the root cause behind fix(mcp): store tool must create a fresh model on every call #735 (singleton-cached repository state) and removes the store-model band-aid. Also collapses the 8 duplicate execute* dispatch methods in McpToolsManager and fixes a latent bug where discover-repositories always reported the base Repository model.
  2. Real MCP errors — every failure path now sets isError=true via Response::error() with a stable {error, code, errors, detail} payload. Actions/getters propagate per-field ValidationException detail like CRUD store/update already did. Raw exception messages/class names only surface under app.debug. Fixes the 'getter' vs 'action' key in the action authorization error.
  3. Tool annotations#[IsReadOnly] / #[IsDestructive] / #[IsIdempotent] / #[IsOpenWorld] on every operation, wrapper, and search tool so hosts get the auto-approve-vs-confirm safety signal. The wrapper catalog (get-repository-operations / get-operation-details) carries the same hints per operation.
  4. Server hardening — default mode flips to wrapper (see breaking change), the per-request ?mode= override is removed (clients could previously expand the exposed tool surface), new restify.mcp.read_only config gates write operations centrally in both discovery and execution, and store file ingestion rejects local filesystem paths by default, blocks private/link-local/loopback IP URLs, and caps download size.
  5. Structured output — tool results emit structuredContent (MCP 2025-06-18) alongside the existing JSON text for deterministic parsing by hosts.
  6. restify:skill command — generates SKILL.md (Agent Skills format: auth, pagination, the filterName=value filter convention, per-repository operations with curl examples) + openapi.json from MCP-enabled repositories.

Breaking change

restify.mcp.mode now defaults to wrapper (4 progressive-disclosure tools) instead of direct (one tool per operation). Research shows tool-selection accuracy collapses with large tool counts, which direct mode produces on any non-trivial app. Set RESTIFY_MCP_MODE=direct to restore the previous behavior.

Test plan

  • Full suite: 465 passed / 4 skipped / 0 failed
  • MCP + Skill suites: 85 passed (512 assertions)
  • Pint clean on all changed files
  • Adversarial review pass (approved after one fix round)

New tests: McpErrorResponseTest, McpServerHardeningTest, McpToolAnnotationsTest, SkillCommandTest, plus regression tests for consecutive stores, cross-repository isolation, structured content, and read-only/SSRF guards.

Known follow-ups (low severity, out of scope)

  • SSRF guard validates DNS then fetches separately (theoretical DNS-rebinding gap); a curl-based fetch pinned to the validated IP would close it.
  • composer analyse is broken repo-wide by a pre-existing invalid PHPStan config key (checkMissingIterableValueType), unrelated to this diff.

binaryk and others added 2 commits July 7, 2026 13:26
- 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.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploying laravel-restify with  Cloudflare Pages  Cloudflare Pages

Latest commit: b3e139e
Status: ✅  Deploy successful!
Preview URL: https://c51f2e8c.laravel-restify.pages.dev
Branch Preview URL: https://feat-mcp-improvements.laravel-restify.pages.dev

View logs

…uard 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.
@binaryk binaryk merged commit c40b0d1 into 10.x Jul 8, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant