Skip to content

Merge PRs #2 to #9 into main - #10

Merged
naji247 merged 263 commits into
mainfrom
feat/plan-a-foundation
Oct 22, 2026
Merged

naji247 merged 263 commits into
mainfrom
feat/plan-a-foundation

Conversation

@naji247

@naji247 naji247 commented Oct 22, 2026

Copy link
Copy Markdown
Member

Summary

PRs #2 to #9 merged into feat/plan-a-foundation, not into main: each one still had the branch of the PR before it as its base. This PR brings them to main.

Test plan

  • CI passes on this PR

…sh timeouts and clear the guard at final shutdown
A jsonSerialize() that returns a new object on every call, or a long chain
of wrappers, made the normalizer recurse until PHP ran out of memory.
Containers and jsonSerialize() expansions now count towards a 512 nesting
cap that is separate from the TS depth; past it a RangeException drops the
event with one 'Failed to truncate event' line. The last-resort clone gets
the same cap.
The last resort collected string paths with array_merge() per entry and
copied each branch by value for every cut, so 16 000 strings of 200 units
took over 6 s. Paths now go into one accumulator and cuts are applied in
place on the private clone. The output is unchanged.
…events

(array) $object names protected and private properties "\0*\0name" and
"\0Class\0name". PHP refuses such keys as property names, so every walker
that built a stdClass from them threw and the event was dropped. Values::entries()
now leaves them out, like the non-public properties they are, and the walkers
read array entries through it.
capture() encoded other values with json_encode() flags that fail on invalid
UTF-8 and on NaN or INF, so a database error array with latin1 bytes or a
NaN ratio came out as [object Object]. The value now goes through WireValue
and the wire encoder flags: bad bytes become U+FFFD, non-finite numbers null,
dates ISO strings and closure properties are left out, as in TS.
The three tool-list injection failure lines called getMessage() directly. A
legacy exception that stores an object as its message made that call throw
out of inject(), and an array message raised a warning. They now use
ErrorText::message(); ordinary messages log the same text as before.
A test process that Infection kills never runs the shutdown cleanup, so its
HOME was left behind in /tmp. The HOME now lives in .cache/test-home/<id>
inside the repo; the cleanup is unchanged and no process sweeps the others.
…nd PHPStan

rector/rector 2.7.0 preloads its bundled phpstan/phpdoc-parser 2.x into
PHPUnit 12+ processes, and phpdocumentor/type-resolver below 1.10 calls
the 1.x ConstExprParser constructor, so mcp/sdk tools/list failed with
-32603 on the lowest set. symfony/polyfill-uuid below 1.20 pulls
paragonie/random_compat 2.0, whose untyped random_bytes() shadows the
built-in for PHPStan, and psr/log 2.0's LoggerTrait leaves log() without
a context value type.
AgentCat's tools/list and tools/call answers reached mcp/sdk's
ResponseEvent listeners as an internal WireResult. They are now
subclasses of ListToolsResult and CallToolResult whose typed fields are
the library's (or the tool's) own result and whose jsonSerialize()
returns the exact wire tree, so the bytes on the wire do not change.
A contract test checks that both parents stay extendable with the same
constructor, and the manifest lists them for the drift job.
AgentCat::track(), flush() and shutdown() created the curl multi handle
in the CurlTransport constructor, so a PHP with curl_multi_init in
disable_functions died with a fatal error before any try block. The
multi handle now starts with the first transfer, and a transport whose
curl functions are missing drops every delivery and logs one line.
…ptions

signals() read Builder::$parts outside StrictErrors, so a read that
warns (a member a new mcp/sdk removed) reached PHP's error handler,
which prints to STDOUT on a stdio server.
They say that AgentCat is off, so a PSR-3 logger that keeps WARNING and
above must still show them. The text is unchanged.
TS listWrap logs its no-tools warning when tools is not an array or is
empty, then serves the answer as is. The PHP listing warned only for an
empty list.
A tracked server is owned by the client that tracked it first. Custom
events for that server (Plan C2) must go out through that client's
publisher, so TrackedServer now holds it.
Every comment in src/ and tests/ now states, in plain words, the rule or the reason it relies on. Decision IDs (D07), bug IDs (B10) and criterion IDs (ac-05-E5) stay. CapturingCallTool names the tested laravel/mcp versions, 1.0.0 to 1.1.0.

Outside comments, unused test group attributes go, and three dataset names and one assertion message are reworded; no src/ code changes. PackageFilesTest guards the committed text of src/, tests/, config/, resources/, .github/, README.md, CHANGELOG.md and CONTRIBUTING.md, a reference that wraps onto the next comment line included.
…he criteria list does

The guard pattern is case-insensitive, and it now reads recipe/ and every committed root file. An explicit list allows the ordinary phrases it would otherwise flag: MCP's own tasks ("task 2 of an MCP server", "task 2 is cancelled") and another document's appendix. A data provider holds positive and negative examples. Comments spell a criterion as the criteria list does (ac-03-11).

Comments only in src/: the docblocks are reflowed, the drift reason is stated once in Drift, and CapturingToolInvoker names laravel/mcp 1.1.0. The package probe lists the dist's top-level entries with bash globs instead of GNU find's -printf.
Composer's pretty version of a tag keeps the tag's v (v0.9.0). The log
prefix, the diagnostics attributes, the events and the User-Agent now
carry the bare version (0.9.0), as the TypeScript SDK names versions, so
one release has one value across SDKs.
The changelog names no Symfony Flex recipe: the recipe reaches Flex after
the release. The tools/list line says in a customer's words that a tool on
a later page keeps its own parameters, that under per-request runtimes the
server's own tools/list handler runs once for each page read during a
tools/call, and what that costs, without IDs a reader cannot look up.

The contributing guide answers issues and pull requests as soon as we can,
with no deadline (D70).
A customer's get_more_tools on a page before the last made the last page
append AgentCat's too, so a client saw two tools of that name. A tracked
server now remembers a page that named the customer's tool and appends
nothing to its last page. A process that never read that page, every
request under PHP-FPM, cannot know it: the shadowing warning now says that
AgentCat's tool is then listed too, on the last page, and to rename the
customer's tool.
…line

With usleep() disabled, a wait for a retry threw inside the transport's
guard, and the delivery loop ran hot until its deadline: about 590,000
passes in the 5 s shutdown budget of a request whose event API failed.
A wait now sleeps with usleep(), or with time_nanosleep() where usleep()
is disabled. Where both are disabled, the wait says it cannot block, one
line per process says why, and the delivery ends at the first retry that
is not due yet.
…es only

A customer's project takes stable releases: with minimum-stability dev, a
dependency that has only dev releases would pass the probe and fail for
customers. The release legs now resolve with stable, and only the dev-main
leg keeps dev.
…ges off STDOUT

The dev-main jobs write PHPUnit's failures, STDERR included and without
colors, to a report that the issue step appends, as the snapshot and
branding jobs do, so the issue names the failing test. The PHP nightly
job prints PHP's own messages to STDERR: a dependency's deprecation can no
longer break the tests that send headers, and the log still shows it.
…d alone

The early return after the customer's own tool had become a second way to
the same answer: the record of that tool already keeps the last page free
of AgentCat's.
…eline

opis/json-schema calls spl_object_hash(), which PHP 8.6 deprecates. PHPUnit
counts the deprecation, and failOnDeprecation fails the PHP 8.6 and PHP
nightly jobs although every test passes. These two jobs now read
phpunit-baseline.xml, which lists only that line. Every other deprecation
still fails them.

The baseline stays in the repository root, because PHPUnit joins the
baseline's directory and each path without resolving "..". It is not in
phpunit.xml.dist, because Infection copies that file to a temporary
directory without rewriting the baseline path. A test reads the file, so a
broken baseline fails on every PHP version, not only in the PHP 8.6 jobs.
…agist

CI tested fixed versions of the MCP libraries: the lowest and highest of each
require-dev range, plus mcp/sdk 0.9.0 pinned. A caret range on 0.x stops at the
next minor, so a new mcp/sdk 0.9.1 or 0.10, symfony/mcp-bundle 0.15 or
laravel/mcp 2.0 never ran, although composer.json lets a customer install it.

Like the TypeScript SDK's mcp-compatibility.yml, a discover job now reads
Packagist on every pull request and every night. It picks the newest patch of
each minor release of mcp/sdk, laravel/mcp and symfony/mcp-bundle, from the
lowest version that require-dev allows. The released jobs run PHPStan and the
adapter suites against each of them, and the package probe runs on each mcp/sdk
release. In the nightly, a failing release opens an issue.

- tests/Support/ReleaseDiscovery.php selects the releases, and
  tests/bin/released-versions.php prints them for the matrix.
- tests/bin/require-library.sh installs one version of a library. The local
  tests/bin/mcp-sdk-leg.sh uses it too.
- tests/Support/HttpGet.php holds the GET that branding-drift.php had inline.
- WorkflowsTest makes sure that All green needs every gating CI job, that the
  released jobs take the discovered releases, and that every nightly job opens
  its issue when it fails.
- The released jobs replace the mcp/sdk 0.9.0 job.
The package probe ran on PHP 8.3 with every discovered mcp/sdk release. A
release that needs PHP 8.4 cannot install there, so every pull request would
fail without an AgentCat defect. ReleaseDiscovery now expands Packagist's
minified rows, reads each release's PHP requirement with composer/semver, and
gives each row the lowest PHP that CI tests and the release allows.

- The nightly also runs the package probe on each discovered mcp/sdk release.
- A nightly issue names the minor release, so a failure that lasts across its
  patches stays in one issue. The issue shows the end of the install, PHPStan
  and PHPUnit output, or the message of released-versions.php.
- released-versions.php tries a failed GET twice more, and exits 1 for an
  unreadable composer.json as its header says.
- The discover steps make sure that the output is a JSON list that is not
  empty before the matrix reads it.
- require-library.sh requires the version without an update, then runs a
  full update, so every root package can move.
- composer/semver is a dev dependency now; it came only through orchestra.
feat: event pipeline, curl transport and shutdown delivery
feat: mcp/sdk adapter and AgentCat::track()
feat: publishCustomEvent, AgentCat::fake(), signal drain and end-to-end tests
feat: exporters (OTLP, Datadog, Sentry, PostHog) and diagnostics
feat: laravel/mcp adapter, Laravel bridge and runtime delivery
Symfony bundle and runtime delivery tests in Docker
Compatibility and release preparation for 1.0.0
ci: discover MCP library releases on Packagist and test each minor
@naji247
naji247 merged commit 482fd55 into main Oct 22, 2026
43 checks passed
@naji247
naji247 deleted the feat/plan-a-foundation branch October 22, 2026 23:11
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.

2 participants