Repository navigation
feat: laravel/mcp adapter, Laravel bridge and runtime delivery - #6
Merged
Merged
Conversation
…re the shutdown delivery
…PHPUnit's buffer PHPUnit 13.4 reports a test as risky when it closes PHPUnit's own output buffer, even when the test opens a new one in its place. The default RequestEnd::closeOutputBuffers() keeps no level, so the in-process test had to close that buffer, and failOnRisky failed the PHP 8.4 and 8.5 highest runs. A PHP process with output_buffering on now runs the close, so the bottom buffer is PHP's own, as in production. The test checks that the buffered text goes out at the close (before a direct STDOUT write), that PHP's buffer and the script's were open, and that no level is left.
…-member contract tests laravel/mcp ^1.0 is a dev dependency, a suggestion and a conflict below 1.0 (D51), with laravel/framework, orchestra/testbench and larastan for the tests and the analysis. The protected members AgentCat reads (Server::$methods, Server::$transport, HttpTransport::$request) are in the manifest, with contract tests, and LaravelTestCase keeps every Laravel write in a directory of its own under the test HOME. symfony/translation gets the dev floor ^6.4.3 || ^7.0.3 || ^8.0. The lowest set reaches v6.4.0 through nesbot/carbon, whatever the laravel/framework or orchestra/testbench floor; that version declares an implicitly nullable parameter in a file the autoloader loads, so PHP 8.4 and 8.5 print a deprecation in every process.
Track laravel/mcp servers through a second library of the registry. The adapter points a server's tools/list and initialize entries, through the public addMethod(), at AgentCat's own method objects, bound in the container under per-server keys; they delegate to the classes the entries named before. A listing gets the injected parameters and get_more_tools on every page; streams, JSON-RPC errors, exceptions and answers without a tool list pass unchanged. initialize records the client of a legacy handshake for the server. JsonTree holds the JSON tree rules both adapters share; mcp/sdk's Trees delegates to it, and McpSdkAdapter::fingerprint(), unused since the registry, is gone. The laravel/mcp harnesses (a new server per HTTP request, one server over stdio) and the tracking fixture share their recording side with the mcp/sdk fixture.
…rrors out of the laravel/mcp drift line
…ders Symfony makes up from the credentials
…ce provider The provider merges and publishes config/agentcat.php (snake_case keys over the Options names, AGENTCAT_* variables, hooks as invokable class names resolved from the current container when they run) and tracks each resolved laravel/mcp server from an afterResolving callback, with the exclude list and the per-server project map. A null exporter entry is skipped without a line, and a header that is not a string is named.
…switch of every client A null projects entry falls back to agentcat.project_id, and agentcat.enabled sets a process-wide switch that TestMode::sdkEnabled() reads before AGENTCAT_ENABLED, so the shared client honors the configuration under config:cache; a fake still records. The tracker asks Laravel for its log channel only while no logger is chosen, and an exporters map of null entries counts as no exporters.
…e-only registration and the --config no-overwrite rule
…response ends with the script
…each runs in its own request An Octane worker serves the next request in the same process. With five deliveries in flight, a waiting event's hooks ran at a later request's pump or wait, so a hook that reads the container (the authenticated user, an injected request) recorded the later request's actor. flush(), the request-end wait, now runs the pipeline of every waiting event whatever the free slots; the payloads wait in order for the transport and go to the endpoint of the moment. pump() and shutdown() still run an event's hooks only when it gets a slot.
…p for a cached configuration With php artisan config:cache Laravel does not load .env, so the names that config/agentcat.php did not map were lost: DISABLE_DIAGNOSTICS (a privacy opt-out that then failed open), AGENTCAT_DIAGNOSTICS_ENDPOINT and _TOKEN, MCPCAT_API_URL, and, for the shared client, the project ID, the API URL and the diagnostics opt-out; diagnostics had no deployment.environment. The file now maps them (diagnostics_endpoint, diagnostics_token, api_url falls back to MCPCAT_API_URL, disable_diagnostics lists both opt-out names and off wins), and the provider hands the configured values and app.env to every client at boot, as TestMode::configure() does for the switch.
…default D60. laravel/mcp builds a server per HTTP request, so AgentCat's setup lines, and an ERROR line for a tool that declares its own session_id, repeated per request in the application's default channel and reached its error trackers. Without log_channel, the bridge now builds a single channel at storage/logs/agentcat.log through Laravel's logging, the analogue of TypeScript's ~/mcpcat.log; log_channel still routes the lines into any application channel.
…d value once A string exclude (App\Mcp\InternalServer::class instead of a list) excluded nothing, so the server was tracked, and a wrong type of projects, project_id, log_channel or the diagnostics keys fell back without a line. A string exclude is now a list of one class; any other wrong type logs one WARN line, once per process, and the default is used (exclude: Laravel Boost; projects: none; an entry of a wrong type is ignored).
The first Diagnostics::init() of the process decides, and a test of another class that tracks with a telemetry client can make it first: the cached-configuration tests that need diagnostics on failed under some random orders (seed 1792379705).
… bad value by its key
'enable_tracing' => env('AGENTCAT_ENABLE_TRACING') with AGENTCAT_ENABLE_TRACING=0 gives the string "0", which logged 'option "enableTracing" must be bool' and left tracing on, against the customer's intent. Every bool key of config/agentcat.php now takes true, 1, yes, on / false, 0, no, off (trimmed, any case) like enabled and disable_diagnostics, a blank value is unset, and a bad value of any key logs one line that names it as written, agentcat.<key>, before the default is used.
…server, so a customer's decorator runs AgentCat answered tools/call with its CapturingCallTool unless CallTool was bound, so an extend() decorator of CallTool, for example an authorization check, and the resolving() callbacks of CallTool were skipped on every tracked server. The container now builds CallTool at each call, as laravel/mcp does: when it builds exactly CallTool, AgentCat answers with CapturingCallTool and keeps the tool's exception; anything else answers itself.
…ss to the events waiting at its start N1: flush(0.0), the console request-end wait and AgentCat::flush(0), ran the hooks of the whole backlog with no time limit before the 5 s shutdown deadline started. Only a flush with a window (a positive timeout, or none) now prepares the waiting events; a 0 s flush runs only the hooks of the events it starts, and the shutdown function of the same request or process runs the others. N2: a hook that queues an event whose hook queues the next one (a redactEvent hook that publishes a custom event) kept a flush from ever returning, which would hang an Octane worker in terminate(). Each prepare pass now takes only the events that waited when it began; later passes take the new ones, and the flush deadline ends the loop.
kashishhora
approved these changes
Oct 22, 2026
Base automatically changed from
feat/plan-d-exporters
to
feat/plan-a-foundation
October 22, 2026 22:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Sixth part of the PHP AgentCat SDK: the
laravel/mcpadapter, the Laravel bridge and runtime delivery. Stacked on #5; review #1 to #5 first.What is in it
composer require agentcat/sdkandAGENTCAT_PROJECT_IDin.env, everylaravel/mcp1.x server of the application is tracked. The auto-discoveredAgentCat\Laravel\AgentCatServiceProvidertracks each server when the container builds it. Booting the provider tracks nothing, so an artisan command or a queue worker that builds no MCP server gets no shutdown function, no signal handlers and no diagnostics.laravel/mcpadapter gives the same events, handles, mint-back and hooks as themcp/sdkadapter. One library-neutral conformance suite runs on both. It pointstools/list,tools/callandinitializeat AgentCat's method objects through per-server container keys. A tool exception keeps its own class, message and frames in the event, and the client still gets laravel/mcp's own text. In a streamed answer, the notifications pass unchanged and the event comes from the final result. A customer's ownCallToolbinding, subclass orextend()decorator still answers.config/agentcat.php(php artisan vendor:publish --tag=agentcat-config):enabled,project_id,exclude(Laravel Boost by default),projects(server class => project ID),log_channel,api_url, the TypeScript options in snake_case, hooks as invokable class names,exporters, and the diagnostics keys. Every AgentCat environment name goes through this file, so it all works underphp artisan config:cache, and theDISABLE_DIAGNOSTICSopt-out holds there too. A value of a wrong type logs one line and never throws.php artisan agentcat:installwrites the project ID into.env.terminating()waits at most 1 s for delivery in a long-running worker (Octane, FrankenPHP worker mode) and makes one non-blocking pass elsewhere. A worker stop delivers what is left. Every waiting event runs its hooks inside its own request, so a hook that reads the request (for example the logged-in user) never records the next request's user.storage/logs/agentcat.logby default;log_channelroutes them to another channel.fastcgi_finish_request()(mod_php,php -S), sends the output buffers first. A tracked laravel/mcp JSON answer gets itsContent-Lengththere, so the client does not wait for delivery. The signal drain does not install under Swoole or OpenSwoole, and FrankenPHP worker mode is detected.laravel-mcp=when laravel/mcp is installed, next tomcp=formcp/sdk.laravel/mcpis an optional dependency (^1.0, conflict<1.0). Contract tests pin every protected member AgentCat reads, on the lowest (1.0.0) and the highest (1.0.2, whosesrc/equalsmainatafcddcf) dependency sets.Testing
2912 tests pass (
composer check; two expected skips). The covered mutation score is 98 % on the full set (5239 mutants) and 98 % on the lines changed since #5. Themcp-sdk-mainjob passes with 649 tests againstmcp/sdkmain at82e19d1. PHP 8.5 with the highest (PHPUnit 13.4.3) and the lowest (PHPUnit 12.5.40) dependency sets passed locally in Docker: both PHPStan configs and 2908 tests each. End-to-end tests run a real Laravel application over HTTP and stdio against a local collector, and the request-end tests ran on PHP 8.3 and 8.5 with pcntl.