*/
+ private $doctrineNoDeprecationsExpectations = [];
+
+ public function expectDeprecationWithIdentifier(string $identifier): void
+ {
+ $this->doctrineDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
+ }
+
+ public function expectNoDeprecationWithIdentifier(string $identifier): void
+ {
+ $this->doctrineNoDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
+ }
+
+ /** @before */
+ #[Before]
+ public function enableDeprecationTracking(): void
+ {
+ Deprecation::enableTrackingDeprecations();
+ }
+
+ /** @after */
+ #[After]
+ public function verifyDeprecationsAreTriggered(): void
+ {
+ foreach ($this->doctrineDeprecationsExpectations as $identifier => $expectation) {
+ $actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
+
+ $this->assertTrue(
+ $actualCount > $expectation,
+ sprintf(
+ "Expected deprecation with identifier '%s' was not triggered by code executed in test.",
+ $identifier
+ )
+ );
+ }
+
+ foreach ($this->doctrineNoDeprecationsExpectations as $identifier => $expectation) {
+ $actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0;
+
+ $this->assertTrue(
+ $actualCount === $expectation,
+ sprintf(
+ "Deprecation with identifier '%s' was triggered by code executed in test, but expected not to.",
+ $identifier
+ )
+ );
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/.gitattributes b/admin/vendor/mcp/sdk/.gitattributes
new file mode 100644
index 0000000..b7b2a18
--- /dev/null
+++ b/admin/vendor/mcp/sdk/.gitattributes
@@ -0,0 +1,12 @@
+/.github export-ignore
+/.phpdoc export-ignore
+/docs export-ignore
+/examples export-ignore
+/tests export-ignore
+/.php-cs-fixer.dist.php export-ignore
+/Makefile export-ignore
+/mkdocs.yml export-ignore
+/requirements-docs.txt export-ignore
+/phpdoc.dist.xml
+/phpstan* export-ignore
+/phpunit.xml.dist export-ignore
diff --git a/admin/vendor/mcp/sdk/.gitignore b/admin/vendor/mcp/sdk/.gitignore
new file mode 100644
index 0000000..ca3a13f
--- /dev/null
+++ b/admin/vendor/mcp/sdk/.gitignore
@@ -0,0 +1,21 @@
+.phpunit.cache
+.php-cs-fixer.cache
+composer.lock
+coverage
+vendor
+examples/**/dev.log
+examples/**/cache
+examples/**/sessions
+tests/Conformance/client-conformance.json
+tests/Conformance/server-conformance.json
+tests/Conformance/results*
+tests/Conformance/sessions
+tests/Conformance/logs/*.log
+
+# phpDocumentor
+.phpdoc/build/
+.phpdoc/cache/
+
+# Documentation site (make docs)
+/site/
+/.cache/
diff --git a/admin/vendor/mcp/sdk/CHANGELOG.md b/admin/vendor/mcp/sdk/CHANGELOG.md
new file mode 100644
index 0000000..f085872
--- /dev/null
+++ b/admin/vendor/mcp/sdk/CHANGELOG.md
@@ -0,0 +1,141 @@
+# Changelog
+
+All notable changes to `mcp/sdk` will be documented in this file.
+
+0.8.0
+-----
+
+* Support for MCP spec version 2026-07-28 incl. stateless HTTP transport
+ * Negotiate the protocol revision during `initialize`: the server counter-offers a revision it supports and the client fails the handshake instead of continuing on an unagreed one. Adds `Client::getProtocolVersion()`.
+ * Serve both protocol eras from one endpoint: `StreamableHttpTransport` classifies each request (`InboundClassifier`) and routes it to the matching dispatcher, so one URL answers modern and handshake-era clients alike. `Builder::withoutModernEra()` opts out, `Builder::setModernVersions()` narrows the modern leg.
+ * Speak the lifecycle from the client: `Client` opens with `server/discover`, stamps `_meta` with version, capabilities and client info, and sends the `Mcp-Method`/`Mcp-Name`/`Mcp-Param-*` headers (via `Client\Stateless\ToolCatalog`). `Schema\Wire\McpHeader` holds the shared header names.
+ * Add multi round-trip requests (SEP-2322): a handler returning `InputRequiredResult` yields `resultType: "input_required"` with an opaque, signed `requestState` (`RequestStateCodec`, key via `Builder::setRequestState()`); the client retries with `inputResponses`, read through `RequestContext::getInputContext()` and its typed `elicitResult()`/`samplingResult()`/`rootsResult()`. On the client, `InputRequestResolver` answers such results automatically from the host's elicitation, sampling and roots handlers.
+ * Serve `ClientGateway::elicit()`/`elicitUrl()` on every revision: where the client cannot be asked mid-request, `Server\Stateless\ElicitationReplay` turns the ask into `input_required` and resumes once re-sent — the handler is entered once per ask. `InputRequiredShim` does the reverse for handshake-era clients. `sample()`/`listRoots()` raise a `LogicException` on `2026-07-28`, which removed them.
+ * Run stateless handlers in a fiber so `$gateway->progress()`/`log()` stream over SSE when the handler emits something and the client accepts `text/event-stream`; honour `io.modelcontextprotocol/logLevel` (SEP-2575). Adds `LoggingLevel::severity()`/`isAtLeast()`.
+ * Deliver notifications on `subscriptions/listen` (SEP-2575) via `NotificationBusInterface` — `InMemoryNotificationBus` for persistent runtimes, `Psr16NotificationBus` for PHP-FPM — set with `Builder::setNotificationBus()`; `Builder::setSubscriptionLifetime()` replaces the hard-coded 30s ceiling.
+ * Validate the standard request headers (SEP-2243) with `StandardHeaderValidator` (`Builder::setHeaderValidator()`), answering `-32020` when they contradict the body.
+ * Add `Wire\CachePolicy` (`Builder::setCachePolicy()`) for SEP-2549 caching hints; defaults to `ttlMs: 0, cacheScope: private`. A `ReadResourceResult` may override with its own values.
+ * Carry W3C trace context (SEP-414): `traceparent`/`tracestate`/`baggage` from `_meta` are exposed via `RequestContext::getTraceContext()` and echoed onto the request's notifications.
+ * Close the schema gaps for `2025-06-18`/`2025-11-25` and add the `2026-07-28` surface (SEP-2106): url-mode elicitation (`ClientGateway::elicitUrl()`/`supportsElicitationUrl()`), `Implementation::title`, and `outputSchema`/`structuredContent` accepting any JSON value.
+ * Deprecate Roots, Sampling and Logging (SEP-2577, earliest removal `2027-07-28`); they keep working but trigger a deprecation notice.
+ * [BC Break] Answer a not-found subject with `-32602` instead of `-32002` (SEP-2164): `resources/read` picks the code by revision (`-32602` from `2026-07-28` on), `prompts/get`, `completion/complete` and `tools/call` switch on every revision. Adds `ProtocolVersion::usesInvalidParamsForResourceNotFound()`.
+ * [BC Break] A list-shaped tool result is only sent as `structuredContent` on `2026-07-28`+; older revisions keep the JSON-encoded value in `content`.
+* [BC Break] Add the extensions framework (SEP-2133) MCP Apps sits on: `ExtensionInterface::getId()` returns an `ExtensionIdentifier` value object, and the interface gains `getMessages()`/`getRequestHandlers()` (extend `AbstractExtension` to skip both). `MessageFactory::make()` takes an `$additional` message list; `RequestHandlerInterface`'s result template is covariant. `ServerExtensionInterface` is replaced by the side-agnostic `Schema\Extension\ExtensionInterface`.
+* Add client-side extension negotiation: `ClientGateway::supportsExtension()`, `Client\Builder::enableExtension()`, `ClientCapabilities::withExtensions()`.
+* Add sampling-with-tools: sampling requests can carry tools and tool-choice preferences, messages support tool-use/tool-result blocks, and clients advertise `sampling.context`/`sampling.tools` (`ClientGateway::supportsSamplingTools()`/`supportsSamplingContext()`). Requests violating the tool-flow rules are rejected with a JSON-RPC error.
+* [BC Break] `SamplingMessage::$content` and `CreateSamplingMessageResult::$content` may be a list of content blocks — use `getContentBlocks()`. `CreateSamplingMessageResult` rejects roles other than `assistant` and empty content.
+* Add client-side Roots support (`RootsCallbackInterface`, `Client::sendRootsListChanged()`) and server-side `ClientGateway::listRoots()`/`supportsRoots()`/`supportsSampling()`.
+* Add `Schema\Content\ResourceLink` to reference a resource by URI in tool results and prompt messages.
+* [BC Break] `Schema\JsonRpc\Error` accepts `null` as `$id`; an unreadable id now omits the member instead of sending `"id": ""`. `MessageFactory` decodes a missing or null id as an id-less error.
+* Preserve the request `id` on an invalid-but-parseable message (`-32600`) via `InvalidInputMessageException::getRequestId()`.
+* [BC Break] Drop the SDK-only name pattern on `ResourceDefinition`/`ResourceTemplate` `$name`; the spec allows any string.
+* Log expected tool failures (`ToolCallException`) at debug level instead of error.
+* Add `annotations` to `ImageContent`.
+* Fix empty tool/resource schemas serializing as `[]` instead of `{}`.
+* Fix `PromptResultFormatter` dropping `annotations`, `_meta` and `mimeType` for plain-array content.
+
+0.7.0
+-----
+
+* Add client-side elicitation support: `ElicitationCallbackInterface`, `ElicitationRequestHandler`, and `ElicitationException` let clients respond to server elicitation requests.
+* Defer element loading to the first registry read: loaders now run at request time (first `has*`/`get*` call) instead of eagerly at `Builder::build()`, fixing empty registries under persistent runtimes (e.g. FrankenPHP worker mode) where a loader's data source is not ready at build time. Adds `Builder::setLazyLoading()` (default on), a public `Registry::load()`, and an optional `LoaderInterface` constructor argument on `Registry`.
+* [BC Break] Element loading is lazy by default: loader failures now surface on the first request rather than at `Builder::build()`, and `initialize` advertises capabilities from the configured sources rather than the loaded registry. Call `Builder::setLazyLoading(false)` to restore eager build-time loading.
+* Allow `[$instance, 'methodName']` as an element handler in `Builder::addTool()`, `addResource()`, `addResourceTemplate()`, and `addPrompt()`. Unblocks handlers with constructor dependencies that the container-less `new $className()` fallback cannot build.
+* Always emit an `items` schema for array tool parameters: untyped arrays get `items: {}` and nullable typed arrays (e.g. `string[]|null`) keep their element type. Fixes strict clients rejecting tools with "array type must have items" (#151).
+* Harden JSON-RPC input parsing: single-message vs batch is now decided from the decoded JSON type (object → single, list array → batch) instead of the raw first byte. Scalars, empty payloads, and non-object batch elements are surfaced as `InvalidInputMessageException` entries instead of triggering warnings or a `TypeError`.
+* Add `maxBatchSize` (default `100`) to `MessageFactory` — oversized JSON-RPC batches are rejected before any message is constructed, guarding against amplification.
+* Add `maxBodyBytes` (default 4 MiB) to `StreamableHttpTransport` — POST bodies exceeding the cap are rejected with `413`. Unknown-size/chunked bodies are read incrementally and stopped at the cap so they cannot exhaust memory.
+* Reject malformed `Mcp-Session-Id` headers with a `400` response: a repeated header or a value that is not a valid UUID is now rejected up front instead of surfacing as an uncaught `Uuid::fromString()` error.
+* Extract RFC 9728 metadata serving into `ProtectedResourceMetadataHandler`, a transport-neutral PSR-15 `RequestHandlerInterface` that can be mounted directly as a Symfony/Laravel controller; `ProtectedResourceMetadataMiddleware` now delegates to it (no BC break).
+
+0.6.0
+-----
+
+* Add `Builder::add(Tool|ResourceDefinition|ResourceTemplate|Prompt $definition, ElementHandlerInterface $handler)` for explicit registration of elements whose schema is only known at runtime.
+* Add handler interfaces `ToolHandlerInterface`, `ResourceHandlerInterface`, `ResourceTemplateHandlerInterface`, `PromptHandlerInterface`, and the `ElementHandlerInterface` marker.
+* [BC Break] Renamed `Mcp\Schema\Resource` to `Mcp\Schema\ResourceDefinition`. No alias.
+* [BC Break] Renamed `Mcp\Capability\Registry\Loader\ArrayLoader` to `Mcp\Capability\Registry\Loader\ReflectedElementLoader`.
+* [BC Break] Bump default protocol version to `2025-11-25`
+* Add support for MCP Apps extension in schema and server
+* Add `extensions` to `ServerCapabilities` and `ClientCapabilities` and `Builder::enableExtension()`
+* Allow overriding the default name pattern for Discovery
+* Add configurable session garbage collection (`gcProbability`/`gcDivisor`)
+* Add optional `title` field to `ResourceDefinition` and `ResourceTemplate` for MCP spec compliance
+* Add `ChainLoader` to compose multiple `LoaderInterface` implementations via explicit ordering.
+* Add `RegistryInterface::unregisterTool()`, `unregisterResource()`, `unregisterResourceTemplate()`, `unregisterPrompt()` — idempotent removals.
+* Add `RegistryInterface::hasTool()`, `hasResource()`, `hasResourceTemplate()`, `hasPrompt()` — by-name existence checks.
+* `DiscoveryLoader` now refreshes only its own previously written entries; manual registrations (via `Builder::addTool()` etc. or runtime `$registry->registerTool()` calls) survive rediscovery, and a same-name manual registration takes precedence over discovery on collision.
+* [BC Break] Removed `ElementReference::$isManual` public property and the `bool $isManual` parameter from all `*Reference` constructors. Origin tracking is no longer carried on the element; manual-over-discovered precedence is encoded by loader execution order.
+* [BC Break] `RegistryInterface::registerTool()`, `registerResource()`, `registerResourceTemplate()`, `registerPrompt()` lost their trailing `bool $isManual = false` parameter. Callers using positional arguments must drop the flag.
+* [BC Break] Removed `RegistryInterface::clear()`, `getDiscoveryState()`, `setDiscoveryState()`. Rediscovery now goes through `DiscoveryLoader::load()` directly.
+* `Registry::register*()` semantics changed to plain last-write-wins (overwrites silently) and the methods now return the stored `*Reference`. The previous "discovered registration is ignored when a manual one already exists" precedence rule still applies, but is now enforced by `DiscoveryLoader` via reference-identity tracking — and still emits a debug log when a discovery is skipped due to a conflicting registration.
+* Add optional `title` parameter to `Builder::addResource()` and `Builder::addResourceTemplate()` for MCP spec compliance
+* [BC Break] `Builder::addResource()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
+* [BC Break] `Builder::addResourceTemplate()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
+* Add `CorsMiddleware`, `DnsRebindingProtectionMiddleware`, and `ProtocolVersionMiddleware` for `StreamableHttpTransport`, composed automatically as the default stack via `StreamableHttpTransport::defaultMiddleware()`
+* [BC BREAK] `StreamableHttpTransport` constructor: `$corsHeaders` parameter removed; CORS is now configured via `CorsMiddleware`. The `$middleware` parameter is nullable — `null` (or omitted) installs the default stack; `[]` disables all defaults. Default `Access-Control-Allow-Origin` is no longer set (was `*`).
+* [BC Break] `ResourceDefinition::__construct()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
+* [BC Break] `ResourceTemplate::__construct()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
+* [BC Break] `McpResource` and `McpResourceTemplate` attribute signatures changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments must switch to named arguments.
+
+0.5.0
+-----
+
+* Add built-in authentication middleware for HTTP transport using OAuth
+* Add client component for building MCP clients
+* Add `Builder::setReferenceHandler()` to allow custom `ReferenceHandlerInterface` implementations (e.g. authorization decorators)
+* Add elicitation enum schema types per SEP-1330: `TitledEnumSchemaDefinition`, `MultiSelectEnumSchemaDefinition`, `TitledMultiSelectEnumSchemaDefinition`
+* [BC break] Make Symfony Finder component optional. Users would need to install `symfony/finder` now themselves
+* Add `LenientOidcDiscoveryMetadataPolicy` for identity providers that omit `code_challenge_methods_supported` (e.g. FusionAuth, Microsoft Entra ID)
+* Add OAuth 2.0 Dynamic Client Registration middleware (RFC 7591)
+* Add optional `title` field to `Prompt` and `McpPrompt` for MCP spec compliance
+* [BC Break] `Builder::addPrompt()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments for `$description` must switch to named arguments.
+* Add optional `title` field to `Tool` and `McpTool` for MCP spec compliance
+* [BC Break] `Tool::__construct()` signature changed — `$title` parameter added between `$name` and `$inputSchema`. Callers using positional arguments must switch to named arguments or pass `null` for `$title`.
+* [BC Break] `McpTool` attribute signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments for `$description` must switch to named arguments.
+* [BC Break] `Builder::addTool()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments for `$description` must switch to named arguments.
+
+0.4.0
+-----
+
+* Rename `Mcp\Server\Session\Psr16StoreSession` to `Mcp\Server\Session\Psr16SessionStore`
+* Add missing handlers for resource subscribe/unsubscribe and persist subscriptions via session
+* Introduce `SessionManager` to encapsulate session handling (replaces `SessionFactory`) and move garbage collection logic from `Protocol`.
+
+0.3.0
+-----
+
+* Add output schema support to MCP tools
+* Add validation of the input parameters given to a Tool.
+* Rename `Mcp\Capability\Registry\ResourceReference::$schema` to `Mcp\Capability\Registry\ResourceReference::$resource`.
+* Introduce `SchemaGeneratorInterface` and `DiscovererInterface` to allow custom schema generation and discovery implementations.
+* Remove `DocBlockParser::getSummary()` method, use `DocBlockParser::getDescription()` instead.
+
+0.2.2
+-----
+
+* Throw exception when trying to inject parameter with the unsupported names `$_session` or `$_request`.
+* `Throwable` objects are passed to log context instead of the exception message.
+
+0.2.1
+-----
+
+* Add `RunnerControl` for `StdioTransport` to allow break out from continuously listening for new input.
+* Open range of supported Symfony versions to include v5.4
+
+0.2.0
+-----
+
+* Make `Protocol` stateless by decouple if from `TransportInterface`. Removed `Protocol::getTransport()`.
+* Change signature of `Builder::addLoaders(...$loaders)` to `Builder::addLoaders(iterable $loaders)`.
+* Removed `ClientAwareInterface` in favor of injecting a `RequestContext` with argument injection.
+* The `ClientGateway` cannot be injected with argument injection anymore. Use `RequestContext` instead.
+* Removed `ClientAwareTrait`
+* Removed `Protocol::getTransport()`
+* Added parameter for `TransportInterface` to `Protocol::processInput()`
+
+0.1.0
+-----
+
+* First tagged release of package
+* Support for implementing MCP server
diff --git a/admin/vendor/mcp/sdk/CONTRIBUTING.md b/admin/vendor/mcp/sdk/CONTRIBUTING.md
new file mode 100644
index 0000000..c02ee47
--- /dev/null
+++ b/admin/vendor/mcp/sdk/CONTRIBUTING.md
@@ -0,0 +1,84 @@
+# Contributing
+
+Thanks for considering a contribution to the PHP SDK for the Model Context Protocol. This is a
+collaboration between [the PHP Foundation](https://thephp.foundation/) and the
+[Symfony project](https://symfony.com/), and it follows Symfony's conventions throughout.
+
+## Ways to contribute
+
+- **Report a bug** or **propose a feature** by [opening an issue](https://github.com/modelcontextprotocol/php-sdk/issues).
+ Check existing issues first — many spec-driven changes are already tracked under the relevant
+ `2026-07-28`-style release label.
+- **Send a pull request** for a fix, a new capability, or a docs improvement.
+- **Improve the guides** under `docs/` — see [Documentation](#documentation) below for how they're built.
+- **Help close a conformance gap** — `make conformance-tests` runs the official MCP conformance
+ suite against this SDK; a failing scenario there is a concrete, well-scoped contribution.
+
+## Development setup
+
+Requires PHP 8.1+.
+
+```bash
+composer install
+```
+
+## Before opening a pull request
+
+Run the full CI suite locally:
+
+```bash
+make ci
+```
+
+This runs, in order: `make cs` (PHP CS Fixer, auto-fixes style), `make phpstan` (static analysis),
+and `make tests` (unit + inspector tests). All three must pass. If your change touches
+protocol-observable behavior, also run:
+
+```bash
+make conformance-tests # requires Docker
+```
+
+## Coding standards
+
+This project follows [Symfony's coding standards](https://symfony.com/doc/current/contributing/code/standards.html)
+and [backward compatibility promise](https://symfony.com/doc/current/contributing/code/bc.html). In short:
+
+See [CLAUDE.md](CLAUDE.md) for a fuller tour of the codebase's architecture and layout.
+
+## Tests
+
+New capabilities need unit tests (`tests/Unit/`) covering the core logic, and — for anything
+reachable over the wire — inspector tests (`tests/Inspector/`) for end-to-end coverage. If you're
+adding a documented pattern, consider adding or updating an example under `examples/`.
+
+## Documentation
+
+The guides under `docs/` are built with [Zensical](https://zensical.org/) in `--strict` mode,
+which fails the build on a broken internal link:
+
+```bash
+make docs-guides
+```
+
+Links between guide pages must be relative paths that resolve within `docs/` (e.g.
+`protocol-versions.md`, `../CLAUDE.md` will *not* resolve — Zensical only follows the `docs/` tree).
+For anything at the repo root (`CLAUDE.md`, `ROADMAP.md`, `CHANGELOG.md`), link to it by its GitHub
+URL instead, matching the pattern already used across `docs/`. The class-level API reference is
+generated separately by phpDocumentor (`make docs-api`) and isn't hand-written.
+
+## Versioning
+
+The SDK follows [Semantic Versioning](https://semver.org/) — see
+[SDK tier target](docs/sdk-tier.md#versioning) for what that means pre- and post-1.0, and how it
+lines up with Symfony's backward compatibility promise.
+
+## Licensing
+
+New contributions are licensed under Apache License, Version 2.0. Existing code predating this
+policy remains under the MIT License — see [LICENSE](LICENSE) for the details. By opening a pull
+request, you agree your contribution is provided under those terms.
+
+## Getting help
+
+If something is unclear or you want early feedback on an approach before writing code, open an
+issue or a draft pull request — that's the right place to ask, rather than guessing at scope.
diff --git a/admin/vendor/mcp/sdk/LICENSE b/admin/vendor/mcp/sdk/LICENSE
new file mode 100644
index 0000000..4a93985
--- /dev/null
+++ b/admin/vendor/mcp/sdk/LICENSE
@@ -0,0 +1,216 @@
+The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
+
+Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
+
+No rights beyond those granted by the applicable original license are conveyed for such contributions.
+
+---
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to the Licensor for inclusion in the Work by the copyright
+ owner or by an individual or Legal Entity authorized to submit on behalf
+ of the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+---
+
+MIT License
+
+Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+---
+
+Creative Commons Attribution 4.0 International (CC-BY-4.0)
+
+Documentation in this project (excluding specifications) is licensed under
+CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
+the full license text.
diff --git a/admin/vendor/mcp/sdk/README.md b/admin/vendor/mcp/sdk/README.md
new file mode 100644
index 0000000..04965f8
--- /dev/null
+++ b/admin/vendor/mcp/sdk/README.md
@@ -0,0 +1,139 @@
+# MCP PHP SDK
+
+
+
+[](https://packagist.org/packages/mcp/sdk)
+[](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/pipeline.yaml)
+[](https://packagist.org/packages/mcp/sdk)
+[](LICENSE)
+
+[](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml)
+[](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml)
+[](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml)
+[](https://github.com/modelcontextprotocol/php-sdk/actions/workflows/conformance-weekly.yaml)
+
+
+
+The official PHP SDK for the Model Context Protocol (MCP). It provides a framework-agnostic API for implementing MCP
+servers and clients in PHP — tools, resources, prompts, STDIO and HTTP transports, sessions, authorization, and both
+protocol eras (the `initialize` handshake and the stateless `2026-07-28` revision).
+
+This project represents a collaboration between [the PHP Foundation](https://thephp.foundation/) and the [Symfony project](https://symfony.com/). It adopts
+development practices and standards from the Symfony project, including [Coding Standards](https://symfony.com/doc/current/contributing/code/standards.html) and the
+[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html).
+
+Until the first major release, this SDK is considered [experimental](https://symfony.com/doc/current/contributing/code/experimental.html), please see the [roadmap](./ROADMAP.md) for
+planned next steps and features.
+
+## Installation
+
+```bash
+composer require mcp/sdk
+```
+
+## Build a server
+
+A server is a plain PHP class plus three lines of wiring:
+
+```php
+use Mcp\Capability\Attribute\McpResource;
+use Mcp\Capability\Attribute\McpTool;
+use Mcp\Server;
+use Mcp\Server\Transport\StdioTransport;
+
+class Calculator
+{
+ /**
+ * Adds two numbers.
+ */
+ #[McpTool]
+ public function add(int $a, int $b): int
+ {
+ return $a + $b;
+ }
+
+ #[McpResource(uri: 'config://calculator/settings')]
+ public function settings(): array
+ {
+ return ['precision' => 2];
+ }
+}
+
+exit(Server::builder()
+ ->setServerInfo('Calculator', '1.0.0')
+ ->setDiscovery(__DIR__, ['.'], excludeDirs: ['vendor'])
+ ->build()
+ ->run(new StdioTransport()));
+```
+
+The walkthrough in [First server](docs/get-started/first-server.md) explains each piece, and
+[Try it with the Inspector](docs/get-started/inspector.md) shows it running.
+
+## Build a client
+
+```php
+use Mcp\Client;
+use Mcp\Client\Transport\StdioTransport;
+
+$client = Client::builder()
+ ->setClientInfo('My Application', '1.0.0')
+ ->build();
+
+$client->connect(new StdioTransport(command: 'php', args: ['/path/to/server.php']));
+
+$tools = $client->listTools();
+$result = $client->callTool('add', ['a' => 5, 'b' => 3]);
+
+$client->disconnect();
+```
+
+See [Connecting to a server](docs/client/connecting.md) for transports, timeouts, and the
+handlers that answer server-initiated requests.
+
+## Documentation
+
+The full documentation is published at **[php.sdk.modelcontextprotocol.io](https://php.sdk.modelcontextprotocol.io/)**.
+
+- **[Get started](docs/get-started/index.md)** — Install the SDK and build your first server
+- **[Servers](docs/servers/index.md)** — Tools, resources, resource templates, prompts, and how to register them
+- **[Inside your handler](docs/handlers/index.md)** — Talking back to the client, logging, and asking for input
+- **[Running your server](docs/run/index.md)** — Server builder, STDIO and HTTP transports, framework integration, sessions, authorization
+- **[Clients](docs/client/index.md)** — Client SDK for connecting to and communicating with MCP servers
+- **[Protocol versions](docs/protocol-versions.md)** — The two protocol eras, and what revision `2026-07-28` changed
+- **[Advanced](docs/advanced/index.md)** — Events, protocol extensions (including MCP Apps), and custom message handlers
+- **[Examples](docs/examples.md)** — Runnable server and client examples
+- **[API Reference](https://php.sdk.modelcontextprotocol.io/api/)** — Generated class reference
+
+## External Resources
+
+- **[Model Context Protocol Documentation](https://modelcontextprotocol.io)** — Official MCP documentation
+- **[Model Context Protocol Specification](https://spec.modelcontextprotocol.io)** — Protocol specification
+- **[Officially Supported Servers](https://github.com/modelcontextprotocol/servers)** — Reference server implementations
+
+## PHP Libraries Using the MCP SDK
+
+- [api-platform/mcp](https://github.com/api-platform/mcp) — MCP integration for API Platform
+- [bnomei/kirby-mcp](https://github.com/bnomei/kirby-mcp) — MCP server for the Kirby CMS
+- [drupal/mcp_server](https://www.drupal.org/project/mcp_server) — MCP server for Drupal exposing configuration and entities as MCP elements
+- [josbeir/cakephp-synapse](https://github.com/josbeir/cakephp-synapse) — CakePHP plugin exposing application functionality over MCP
+- [nette/mcp-inspector](https://github.com/nette/mcp-inspector) — MCP server for introspecting Nette applications
+- [symfony/ai-mate](https://github.com/symfony/ai-mate) — AI development assistant MCP server for Symfony projects
+- [symfony/mcp-bundle](https://github.com/symfony/mcp-bundle) — Symfony integration bundle
+
+Building something on top of the SDK? Open a pull request to add it to this list.
+
+## Contributing
+
+We are passionate about supporting contributors of all levels of experience and would love to see you get involved in
+the project. Start by [reporting issues](https://github.com/modelcontextprotocol/php-sdk/issues) or
+[sending pull requests](https://github.com/modelcontextprotocol/php-sdk/pulls). See
+[CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding standards, and what to run before
+opening a PR.
+
+## Credits
+
+The starting point for this SDK was the [PHP-MCP](https://github.com/php-mcp/server) project, initiated by [Kyrian Obikwelu](https://github.com/CodeWithKyrian), and the [Symfony AI initiative](https://github.com/symfony/ai). We are grateful for the work done by both projects and their contributors, which created a solid foundation for this SDK.
+
+## License
+
+This project is licensed under the Apache License, Version 2.0 for new contributions, with existing code under the MIT License — see the [LICENSE](LICENSE) file for details.
diff --git a/admin/vendor/mcp/sdk/ROADMAP.md b/admin/vendor/mcp/sdk/ROADMAP.md
new file mode 100644
index 0000000..5dfb9ce
--- /dev/null
+++ b/admin/vendor/mcp/sdk/ROADMAP.md
@@ -0,0 +1,21 @@
+# Roadmap
+
+This roadmap is a living document that outlines the planned features and improvements for our project.
+
+## Goals for the First Major Release
+
+- **Server**
+- [x] Implement full support for elicitations
+- [ ] Implement OAuth2 authentication for server
+- **Client**
+- [x] Implement client-side support
+- [x] Implement client examples and documentation
+- [ ] Implement OAuth2 authentication for client
+- **Schema**
+- [ ] Implement schema generation based on TS or JSON Schema
+- [ ] Achieve full spec conformance
+- **Extensions**
+- [ ] Implement the Tasks extension
+- **Architecture**
+- [ ] Split the SDK into multiple packages
+
diff --git a/admin/vendor/mcp/sdk/SECURITY.md b/admin/vendor/mcp/sdk/SECURITY.md
new file mode 100644
index 0000000..5029242
--- /dev/null
+++ b/admin/vendor/mcp/sdk/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+Thank you for helping keep the Model Context Protocol and its ecosystem secure.
+
+## Reporting Security Issues
+
+If you discover a security vulnerability in this repository, please report it through
+the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)
+for this repository.
+
+Please **do not** report security vulnerabilities through public GitHub issues, discussions,
+or pull requests.
+
+## What to Include
+
+To help us triage and respond quickly, please include:
+
+- A description of the vulnerability
+- Steps to reproduce the issue
+- The potential impact
+- Any suggested fixes (optional)
diff --git a/admin/vendor/mcp/sdk/adr/0001-oauth-authorization-server-out-of-scope.md b/admin/vendor/mcp/sdk/adr/0001-oauth-authorization-server-out-of-scope.md
new file mode 100644
index 0000000..ebb5947
--- /dev/null
+++ b/admin/vendor/mcp/sdk/adr/0001-oauth-authorization-server-out-of-scope.md
@@ -0,0 +1,97 @@
+# 0001 — The MCP server is an OAuth Resource Server, not an Authorization Server
+
+- Status: Accepted
+- Date: 2026-06-15
+
+## Context
+
+OAuth 2.1 defines three distinct roles:
+
+| Role | What it does | Status in this SDK | Scope |
+|------|--------------|--------------------|-------|
+| Resource Server | Validates incoming bearer tokens, serves Protected Resource Metadata (RFC 9728), emits `WWW-Authenticate` | Shipped (`AuthorizationMiddleware`, `JwtTokenValidator`, `ProtectedResourceMetadata`) | **IN scope** |
+| Delegation / proxy to an upstream AS | Forwards `/authorize` and `/token` to your existing IdP | Shipped (`OAuthProxyMiddleware`) | **IN scope — delegation ONLY** |
+| Authorization Server / Identity Provider (IdP) | Mints its own tokens, registers clients, runs login and consent | Absent | **OUT of scope** |
+
+The SDK repeatedly receives pull requests that move it toward becoming a full OAuth 2.1
+**authorization server** — asking the MCP server to mint its own access and refresh tokens,
+register clients, and run login and consent flows, that is, to become an Identity Provider.
+The most explicit example is [#373](https://github.com/modelcontextprotocol/php-sdk/pull/373)
+("[Server] Add native OAuth 2.1 authorization server", ~3,400 lines across 50+ files), but the
+direction has crept in incrementally rather than in one PR.
+
+A contributing factor is that the SDK already exposes the *authorization-server endpoints in
+proxy form*, and that surface has grown:
+[#221](https://github.com/modelcontextprotocol/php-sdk/pull/221) added the OAuth resource-server
+middleware, and [#269](https://github.com/modelcontextprotocol/php-sdk/pull/269) added Dynamic
+Client Registration (RFC 7591). Today `OAuthProxyMiddleware` answers `/authorize`, `/token`, and
+`/.well-known/oauth-authorization-server`, and DCR endpoints exist. The shape of that surface
+invites contributors to "finish the job" by backing those endpoints with a real token issuer.
+It is not scaffolding to be completed — it is a delegating proxy, and that is the whole of its
+intent.
+
+## Decision
+
+**The MCP server is an OAuth 2.1 Resource Server that MAY delegate to an upstream
+authorization server. It will NOT issue tokens or act as an Identity Provider.**
+
+Concretely:
+
+- The SDK validates bearer tokens, serves RFC 9728 Protected Resource Metadata, and emits
+ `WWW-Authenticate` challenges. This is the Resource Server role and is fully supported.
+- The SDK MAY delegate `/authorize` and `/token` to an upstream authorization server via
+ `OAuthProxyMiddleware`. This is delegation only: the middleware redirects the browser to
+ the upstream `/authorize` endpoint and proxies `/token` requests to the upstream token
+ endpoint. It never mints, signs, stores, or rotates tokens of its own.
+- The SDK will NOT implement an authorization server: no token issuance, no token signing or
+ key management, no login UI, no consent UI, no authorization-code or refresh-token storage,
+ no first-party Dynamic Client Registration acting as an issuer.
+
+Pull requests that add authorization-server / IdP behavior are declined by reference to this
+ADR.
+
+## Rationale
+
+- **Security liability.** Issuing tokens means owning signing-key generation, storage, and
+ rotation; authorization-code and refresh-token persistence; refresh-token rotation and
+ replay detection; and consent. A defect in any of these is a credential-issuance
+ vulnerability affecting every consumer of the SDK. This is precisely the surface an MCP SDK
+ should not own.
+- **RFC footprint.** A correct authorization server must implement and keep current with
+ RFC 6749 (OAuth 2.0), RFC 7591 (Dynamic Client Registration), RFC 8414 (Authorization
+ Server Metadata), PKCE (RFC 7636), and refresh-token rotation guidance, among others. That
+ is an open-ended maintenance and conformance burden far outside the SDK's purpose.
+- **Mature implementations already exist.** Token issuance is a solved problem.
+ `league/oauth2-server` provides it as a PHP library, and every production IdP — Keycloak,
+ Auth0, Microsoft Entra ID, Okta — provides it as a service. Re-implementing it inside an
+ MCP SDK adds risk without adding value.
+
+## Boundary statement
+
+`OAuthProxyMiddleware` **delegates** to an upstream IdP. It is **not** authorization-server
+scaffolding to be completed. Backing its `/authorize` and `/token` endpoints with a
+first-party token issuer is out of scope and will be declined.
+
+## Consequences
+
+- The supported authorization architecture is: an external authorization server (your IdP or
+ `league/oauth2-server` running in your own application) issues tokens; the MCP server
+ validates them as a Resource Server and optionally proxies the OAuth flow to that upstream.
+- Contributors get a single, citable ruling for why authorization-server PRs are declined,
+ reducing repeated large-PR churn.
+- Resource Server and proxy/delegation features remain welcome and supported.
+
+## Alternatives / what to do instead
+
+If you need an authorization server (token issuance, client registration, login, consent), do
+**not** add it to this SDK. Instead:
+
+- **Front the MCP server with an existing IdP** — Keycloak, Auth0, Microsoft Entra ID, or
+ Okta. Point `JwtTokenValidator` and `ProtectedResourceMetadata` at that issuer, and
+ optionally use `OAuthProxyMiddleware` to delegate `/authorize` and `/token` to it.
+- **Run `league/oauth2-server` in your own application**, behind the SDK's existing proxy and
+ validator seams. The MCP server validates the tokens it issues; it does not issue them
+ itself.
+
+See [`../docs/run/authorization.md`](../docs/run/authorization.md) for the supported Resource Server
+and delegation setup.
diff --git a/admin/vendor/mcp/sdk/adr/README.md b/admin/vendor/mcp/sdk/adr/README.md
new file mode 100644
index 0000000..d8ab0de
--- /dev/null
+++ b/admin/vendor/mcp/sdk/adr/README.md
@@ -0,0 +1,10 @@
+# Architecture Decision Records
+
+This directory holds Architecture Decision Records (ADRs): short documents that capture a
+significant architectural decision, the context that led to it, and its consequences. They
+exist so a decision can be cited once and referenced thereafter — for example, when declining
+a pull request that conflicts with an accepted decision.
+
+## Records
+
+- [0001 — The MCP server is an OAuth Resource Server, not an Authorization Server](0001-oauth-authorization-server-out-of-scope.md)
diff --git a/admin/vendor/mcp/sdk/composer.json b/admin/vendor/mcp/sdk/composer.json
new file mode 100644
index 0000000..5c572b0
--- /dev/null
+++ b/admin/vendor/mcp/sdk/composer.json
@@ -0,0 +1,97 @@
+{
+ "name": "mcp/sdk",
+ "description": "Model Context Protocol SDK for Client and Server applications in PHP",
+ "license": "Apache-2.0",
+ "type": "library",
+ "authors": [
+ {
+ "name": "Christopher Hertel",
+ "email": "mail@christopher-hertel.de"
+ },
+ {
+ "name": "Kyrian Obikwelu",
+ "email": "koshnawaza@gmail.com"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com"
+ }
+ ],
+ "require": {
+ "php": "^8.1",
+ "ext-fileinfo": "*",
+ "opis/json-schema": "^2.4",
+ "php-http/discovery": "^1.20",
+ "phpdocumentor/reflection-docblock": "^5.6 || ^6.0",
+ "psr/clock": "^1.0",
+ "psr/container": "^1.0 || ^2.0",
+ "psr/event-dispatcher": "^1.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.1",
+ "psr/http-message": "^1.1 || ^2.0",
+ "psr/http-server-handler": "^1.0",
+ "psr/http-server-middleware": "^1.0",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3.0",
+ "symfony/uid": "^5.4 || ^6.4 || ^7.3 || ^8.0"
+ },
+ "suggest": {
+ "symfony/finder": "Required for file-based discovery."
+ },
+ "require-dev": {
+ "ext-openssl": "*",
+ "composer/semver": "^3.0",
+ "firebase/php-jwt": "^6.10 || ^7.0",
+ "laminas/laminas-httphandlerrunner": "^2.12",
+ "nyholm/psr7": "^1.8",
+ "nyholm/psr7-server": "^1.1",
+ "phar-io/composer-distributor": "^1.0.2",
+ "php-cs-fixer/shim": "^3.91",
+ "phpdocumentor/shim": "^3",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^10.5",
+ "psr/simple-cache": "^2.0 || ^3.0",
+ "symfony/cache": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/console": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/finder": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/http-client": "^5.4 || ^6.4 || ^7.3 || ^8.0",
+ "symfony/process": "^5.4 || ^6.4 || ^7.3 || ^8.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Mcp\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Mcp\\Example\\Server\\CachedDiscovery\\": "examples/server/cached-discovery/",
+ "Mcp\\Example\\Server\\ClientCommunication\\": "examples/server/client-communication/",
+ "Mcp\\Example\\Server\\ClientLogging\\": "examples/server/client-logging/",
+ "Mcp\\Example\\Server\\CombinedRegistration\\": "examples/server/combined-registration/",
+ "Mcp\\Example\\Server\\Elicitation\\": "examples/server/elicitation/",
+ "Mcp\\Example\\Server\\ComplexToolSchema\\": "examples/server/complex-tool-schema/",
+ "Mcp\\Example\\Server\\Conformance\\": "examples/server/conformance/",
+ "Mcp\\Example\\Server\\CustomDependencies\\": "examples/server/custom-dependencies/",
+ "Mcp\\Example\\Server\\CustomMethodHandlers\\": "examples/server/custom-method-handlers/",
+ "Mcp\\Example\\Server\\DiscoveryCalculator\\": "examples/server/discovery-calculator/",
+ "Mcp\\Example\\Server\\DiscoveryUserProfile\\": "examples/server/discovery-userprofile/",
+ "Mcp\\Example\\Server\\EnvVariables\\": "examples/server/env-variables/",
+ "Mcp\\Example\\Server\\ExplicitRegistration\\": "examples/server/explicit-registration/",
+ "Mcp\\Example\\Server\\McpApps\\": "examples/server/mcp-apps/",
+ "Mcp\\Example\\Server\\OAuthKeycloak\\": "examples/server/oauth-keycloak/",
+ "Mcp\\Example\\Server\\OAuthMicrosoft\\": "examples/server/oauth-microsoft/",
+ "Mcp\\Example\\Server\\SchemaShowcase\\": "examples/server/schema-showcase/",
+ "Mcp\\Tests\\": "tests/"
+ },
+ "classmap": [
+ "tests/Unit/Capability/Discovery/Fixtures/AlternativeFileNameToolHandler.class.inc"
+ ]
+ },
+ "config": {
+ "allow-plugins": {
+ "php-http/discovery": false,
+ "phpdocumentor/shim": true
+ },
+ "sort-packages": true
+ }
+}
diff --git a/admin/vendor/mcp/sdk/phpdoc.dist.xml b/admin/vendor/mcp/sdk/phpdoc.dist.xml
new file mode 100644
index 0000000..7c705bd
--- /dev/null
+++ b/admin/vendor/mcp/sdk/phpdoc.dist.xml
@@ -0,0 +1,41 @@
+
+
+ MCP PHP SDK
+
+
+
+
+
+
+
+ src
+
+
+ vendor/**/*
+ tests/**/*
+
+
+ phpstan-type
+ phpstan-type-import
+ template
+ template-covariant
+ template-extends
+ template-implements
+ extends
+ implements
+
+
+
+
+
+
+
diff --git a/admin/vendor/mcp/sdk/src/Capability/Attribute/CompletionProvider.php b/admin/vendor/mcp/sdk/src/Capability/Attribute/CompletionProvider.php
new file mode 100644
index 0000000..9e8dc80
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Attribute/CompletionProvider.php
@@ -0,0 +1,38 @@
+
+ */
+#[\Attribute(\Attribute::TARGET_PARAMETER)]
+class CompletionProvider
+{
+ /**
+ * @param class-string|ProviderInterface|null $provider if a class-string, it will be resolved
+ * from the container at the point of use
+ * @param ?array $values a list of values to use for completion
+ */
+ public function __construct(
+ public ?string $providerClass = null,
+ public string|ProviderInterface|null $provider = null,
+ public ?array $values = null,
+ public ?string $enum = null,
+ ) {
+ if (1 !== \count(array_filter([$provider, $values, $enum]))) {
+ throw new InvalidArgumentException('Only one of provider, values, or enum can be set.');
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Attribute/McpPrompt.php b/admin/vendor/mcp/sdk/src/Capability/Attribute/McpPrompt.php
new file mode 100644
index 0000000..cd47fe2
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Attribute/McpPrompt.php
@@ -0,0 +1,40 @@
+
+ */
+#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
+class McpPrompt
+{
+ /**
+ * @param ?string $name overrides the prompt name (defaults to method name)
+ * @param ?string $title Optional human-readable title for display in UI
+ * @param ?string $description Optional description of the prompt. Defaults to method DocBlock summary.
+ * @param ?Icon[] $icons Optional list of icon URLs representing the prompt
+ * @param ?array $meta Optional metadata
+ */
+ public function __construct(
+ public ?string $name = null,
+ public ?string $title = null,
+ public ?string $description = null,
+ public ?array $icons = null,
+ public ?array $meta = null,
+ ) {
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Attribute/McpResource.php b/admin/vendor/mcp/sdk/src/Capability/Attribute/McpResource.php
new file mode 100644
index 0000000..0f51657
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Attribute/McpResource.php
@@ -0,0 +1,49 @@
+
+ */
+#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
+class McpResource
+{
+ /**
+ * @param string $uri the specific URI identifying this resource instance
+ * @param ?string $name a short identifier for this resource; defaults to the method name
+ * @param ?string $title optional human-readable title for display in UI
+ * @param ?string $description optional description; defaults to class DocBlock summary
+ * @param ?string $mimeType the MIME type, if known and constant for this resource
+ * @param ?int $size the size in bytes, if known and constant
+ * @param ?Annotations $annotations optional annotations describing the resource
+ * @param ?Icon[] $icons optional icons representing the resource
+ * @param ?array $meta optional metadata
+ */
+ public function __construct(
+ public string $uri,
+ public ?string $name = null,
+ public ?string $title = null,
+ public ?string $description = null,
+ public ?string $mimeType = null,
+ public ?int $size = null,
+ public ?Annotations $annotations = null,
+ public ?array $icons = null,
+ public ?array $meta = null,
+ ) {
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Attribute/McpResourceTemplate.php b/admin/vendor/mcp/sdk/src/Capability/Attribute/McpResourceTemplate.php
new file mode 100644
index 0000000..6a2044d
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Attribute/McpResourceTemplate.php
@@ -0,0 +1,44 @@
+
+ */
+#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
+class McpResourceTemplate
+{
+ /**
+ * @param string $uriTemplate the URI template string (RFC 6570)
+ * @param ?string $name a short identifier for the template type; defaults to the method name
+ * @param ?string $title optional human-readable title for display in UI
+ * @param ?string $description optional description; defaults to class DocBlock summary
+ * @param ?string $mimeType optional default MIME type for matching resources
+ * @param ?Annotations $annotations optional annotations describing the resource template
+ * @param ?array $meta optional metadata
+ */
+ public function __construct(
+ public string $uriTemplate,
+ public ?string $name = null,
+ public ?string $title = null,
+ public ?string $description = null,
+ public ?string $mimeType = null,
+ public ?Annotations $annotations = null,
+ public ?array $meta = null,
+ ) {
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Attribute/McpTool.php b/admin/vendor/mcp/sdk/src/Capability/Attribute/McpTool.php
new file mode 100644
index 0000000..1c71e2c
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Attribute/McpTool.php
@@ -0,0 +1,42 @@
+
+ */
+#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_CLASS)]
+class McpTool
+{
+ /**
+ * @param string|null $name The name of the tool (defaults to the method name)
+ * @param string|null $title Optional human-readable title for display in UI
+ * @param string|null $description The description of the tool (defaults to the DocBlock/inferred)
+ * @param ToolAnnotations|null $annotations Optional annotations describing tool behavior
+ * @param ?Icon[] $icons Optional list of icon URLs representing the tool
+ * @param ?array $meta Optional metadata
+ * @param array $outputSchema Optional JSON Schema object for defining the expected output structure
+ */
+ public function __construct(
+ public ?string $name = null,
+ public ?string $title = null,
+ public ?string $description = null,
+ public ?ToolAnnotations $annotations = null,
+ public ?array $icons = null,
+ public ?array $meta = null,
+ public ?array $outputSchema = null,
+ ) {
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Attribute/Schema.php b/admin/vendor/mcp/sdk/src/Capability/Attribute/Schema.php
new file mode 100644
index 0000000..80ec4b5
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Attribute/Schema.php
@@ -0,0 +1,258 @@
+,
+ * type?: string,
+ * description?: string,
+ * enum?: array,
+ * format?: string,
+ * minLength?: int,
+ * maxLength?: int,
+ * pattern?: string,
+ * minimum?: int,
+ * maximum?: int,
+ * exclusiveMinimum?: int,
+ * exclusiveMaximum?: int,
+ * multipleOf?: int|float,
+ * items?: array,
+ * minItems?: int,
+ * maxItems?: int,
+ * uniqueItems?: bool,
+ * properties?: array,
+ * required?: array,
+ * additionalProperties?: bool|array,
+ * }
+ *
+ * @author Kyrian Obikwelu
+ */
+#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
+class Schema
+{
+ /**
+ * The complete JSON schema array.
+ * If provided, it takes precedence over individual properties like $type, $properties, etc.
+ *
+ * @var ?array
+ */
+ public ?array $definition = null;
+
+ /**
+ * Alternatively, provide individual top-level schema keywords.
+ * These are used if $definition is null.
+ */
+ public ?string $type = null;
+ public ?string $description = null;
+ public mixed $default = null;
+ /**
+ * @var ?array
+ */
+ public ?array $enum = null; // list of allowed values
+ public ?string $format = null; // e.g., 'email', 'date-time'
+
+ // Constraints for string
+ public ?int $minLength = null;
+ public ?int $maxLength = null;
+ public ?string $pattern = null;
+
+ // Constraints for number/integer
+ public int|float|null $minimum = null;
+ public int|float|null $maximum = null;
+ public ?bool $exclusiveMinimum = null;
+ public ?bool $exclusiveMaximum = null;
+ public int|float|null $multipleOf = null;
+
+ // Constraints for array
+ /**
+ * @var ?array
+ */
+ public ?array $items = null; // JSON schema for array items
+ public ?int $minItems = null;
+ public ?int $maxItems = null;
+ public ?bool $uniqueItems = null;
+
+ // Constraints for object (primarily used when Schema is on a method or an object-typed parameter)
+ /**
+ * @var ?array
+ */
+ public ?array $properties = null; // [propertyName => [schema array], ...]
+ /**
+ * @var ?array
+ */
+ public ?array $required = null; // [propertyName, ...]
+ /**
+ * @var bool|array|null
+ */
+ public bool|array|null $additionalProperties = null; // true, false, or a schema array
+
+ /**
+ * @param ?array $definition A complete JSON schema array. If provided, other parameters are ignored.
+ * @param ?string $type the JSON schema type
+ * @param ?string $description description of the element
+ * @param ?array $enum allowed enum values
+ * @param ?string $format String format (e.g., 'date-time', 'email').
+ * @param ?int $minLength minimum length for strings
+ * @param ?int $maxLength maximum length for strings
+ * @param ?string $pattern regex pattern for strings
+ * @param int|float|null $minimum minimum value for numbers/integers
+ * @param int|float|null $maximum maximum value for numbers/integers
+ * @param ?bool $exclusiveMinimum exclusive minimum
+ * @param ?bool $exclusiveMaximum exclusive maximum
+ * @param int|float|null $multipleOf must be a multiple of this value
+ * @param ?array $items JSON Schema for items if type is 'array'
+ * @param ?int $minItems minimum items for an array
+ * @param ?int $maxItems maximum items for an array
+ * @param ?bool $uniqueItems whether array items must be unique
+ * @param ?array $properties Property definitions if type is 'object'. [name => schema_array].
+ * @param ?array $required list of required properties for an object
+ * @param bool|array|null $additionalProperties policy for additional properties in an object
+ */
+ public function __construct(
+ ?array $definition = null,
+ ?string $type = null,
+ ?string $description = null,
+ ?array $enum = null,
+ ?string $format = null,
+ ?int $minLength = null,
+ ?int $maxLength = null,
+ ?string $pattern = null,
+ int|float|null $minimum = null,
+ int|float|null $maximum = null,
+ ?bool $exclusiveMinimum = null,
+ ?bool $exclusiveMaximum = null,
+ int|float|null $multipleOf = null,
+ ?array $items = null,
+ ?int $minItems = null,
+ ?int $maxItems = null,
+ ?bool $uniqueItems = null,
+ ?array $properties = null,
+ ?array $required = null,
+ bool|array|null $additionalProperties = null,
+ ) {
+ if (null !== $definition) {
+ $this->definition = $definition;
+ } else {
+ $this->type = $type;
+ $this->description = $description;
+ $this->enum = $enum;
+ $this->format = $format;
+ $this->minLength = $minLength;
+ $this->maxLength = $maxLength;
+ $this->pattern = $pattern;
+ $this->minimum = $minimum;
+ $this->maximum = $maximum;
+ $this->exclusiveMinimum = $exclusiveMinimum;
+ $this->exclusiveMaximum = $exclusiveMaximum;
+ $this->multipleOf = $multipleOf;
+ $this->items = $items;
+ $this->minItems = $minItems;
+ $this->maxItems = $maxItems;
+ $this->uniqueItems = $uniqueItems;
+ $this->properties = $properties;
+ $this->required = $required;
+ $this->additionalProperties = $additionalProperties;
+ }
+ }
+
+ /**
+ * Converts the attribute's definition to a JSON schema array.
+ *
+ * @return SchemaAttributeData
+ */
+ public function toArray(): array
+ {
+ if (null !== $this->definition) {
+ return [
+ 'definition' => $this->definition,
+ ];
+ }
+
+ $schema = [];
+ if (null !== $this->type) {
+ $schema['type'] = $this->type;
+ }
+ if (null !== $this->description) {
+ $schema['description'] = $this->description;
+ }
+ if (null !== $this->enum) {
+ $schema['enum'] = $this->enum;
+ }
+ if (null !== $this->format) {
+ $schema['format'] = $this->format;
+ }
+
+ // String
+ if (null !== $this->minLength) {
+ $schema['minLength'] = $this->minLength;
+ }
+ if (null !== $this->maxLength) {
+ $schema['maxLength'] = $this->maxLength;
+ }
+ if (null !== $this->pattern) {
+ $schema['pattern'] = $this->pattern;
+ }
+
+ // Numeric
+ if (null !== $this->minimum) {
+ $schema['minimum'] = $this->minimum;
+ }
+ if (null !== $this->maximum) {
+ $schema['maximum'] = $this->maximum;
+ }
+ if (null !== $this->exclusiveMinimum) {
+ $schema['exclusiveMinimum'] = $this->exclusiveMinimum;
+ }
+ if (null !== $this->exclusiveMaximum) {
+ $schema['exclusiveMaximum'] = $this->exclusiveMaximum;
+ }
+ if (null !== $this->multipleOf) {
+ $schema['multipleOf'] = $this->multipleOf;
+ }
+
+ // Array
+ if (null !== $this->items) {
+ $schema['items'] = $this->items;
+ }
+ if (null !== $this->minItems) {
+ $schema['minItems'] = $this->minItems;
+ }
+ if (null !== $this->maxItems) {
+ $schema['maxItems'] = $this->maxItems;
+ }
+ if (null !== $this->uniqueItems) {
+ $schema['uniqueItems'] = $this->uniqueItems;
+ }
+
+ // Object
+ if (null !== $this->properties) {
+ $schema['properties'] = $this->properties;
+ }
+ if (null !== $this->required) {
+ $schema['required'] = $this->required;
+ }
+ if (null !== $this->additionalProperties) {
+ $schema['additionalProperties'] = $this->additionalProperties;
+ }
+
+ return $schema;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Completion/EnumCompletionProvider.php b/admin/vendor/mcp/sdk/src/Capability/Completion/EnumCompletionProvider.php
new file mode 100644
index 0000000..7864e4e
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Completion/EnumCompletionProvider.php
@@ -0,0 +1,52 @@
+
+ */
+class EnumCompletionProvider implements ProviderInterface
+{
+ /**
+ * @var string[]
+ */
+ private array $values;
+
+ /**
+ * @param class-string $enumClass
+ */
+ public function __construct(string $enumClass)
+ {
+ if (!enum_exists($enumClass)) {
+ throw new InvalidArgumentException(\sprintf('Class "%s" is not an enum.', $enumClass));
+ }
+
+ $this->values = array_map(
+ static fn ($case) => isset($case->value) && \is_string($case->value) ? $case->value : $case->name,
+ $enumClass::cases()
+ );
+ }
+
+ public function getCompletions(string $currentValue): array
+ {
+ if (empty($currentValue)) {
+ return $this->values;
+ }
+
+ return array_values(array_filter(
+ $this->values,
+ static fn (string $value) => str_starts_with($value, $currentValue)
+ ));
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Completion/ListCompletionProvider.php b/admin/vendor/mcp/sdk/src/Capability/Completion/ListCompletionProvider.php
new file mode 100644
index 0000000..5d48f4b
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Completion/ListCompletionProvider.php
@@ -0,0 +1,38 @@
+
+ */
+class ListCompletionProvider implements ProviderInterface
+{
+ /**
+ * @param string[] $values
+ */
+ public function __construct(
+ private array $values,
+ ) {
+ }
+
+ public function getCompletions(string $currentValue): array
+ {
+ if (empty($currentValue)) {
+ return $this->values;
+ }
+
+ return array_values(array_filter(
+ $this->values,
+ static fn (string $value) => str_starts_with($value, $currentValue)
+ ));
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Completion/ProviderInterface.php b/admin/vendor/mcp/sdk/src/Capability/Completion/ProviderInterface.php
new file mode 100644
index 0000000..84f3f23
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Completion/ProviderInterface.php
@@ -0,0 +1,27 @@
+
+ */
+interface ProviderInterface
+{
+ /**
+ * Get completions for a given current value.
+ *
+ * @param string $currentValue the current value to get completions for
+ *
+ * @return string[] the completions
+ */
+ public function getCompletions(string $currentValue): array;
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/CachedDiscoverer.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/CachedDiscoverer.php
new file mode 100644
index 0000000..d5ed852
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/CachedDiscoverer.php
@@ -0,0 +1,100 @@
+
+ */
+final class CachedDiscoverer implements DiscovererInterface
+{
+ private const CACHE_PREFIX = 'mcp_discovery_';
+
+ public function __construct(
+ private readonly DiscovererInterface $discoverer,
+ private readonly CacheInterface $cache,
+ private readonly LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * Discover MCP elements in the specified directories with caching.
+ *
+ * @param string $basePath the base path for resolving directories
+ * @param array $directories list of directories (relative to base path) to scan
+ * @param array $excludeDirs list of directories (relative to base path) to exclude from the scan
+ * @param array $namePatterns list of file name patterns for the scan. Compatible with Finder->name()
+ */
+ public function discover(string $basePath, array $directories, array $excludeDirs = [], array $namePatterns = self::DEFAULT_NAME_PATERNS): DiscoveryState
+ {
+ $cacheKey = $this->generateCacheKey($basePath, $directories, $excludeDirs);
+
+ $cachedResult = $this->cache->get($cacheKey);
+ if (null !== $cachedResult) {
+ $this->logger->debug('Using cached discovery results', [
+ 'cache_key' => $cacheKey,
+ 'base_path' => $basePath,
+ 'directories' => $directories,
+ ]);
+
+ return $cachedResult;
+ }
+
+ $this->logger->debug('Cache miss, performing fresh discovery', [
+ 'cache_key' => $cacheKey,
+ 'base_path' => $basePath,
+ 'directories' => $directories,
+ ]);
+
+ $discoveryState = $this->discoverer->discover($basePath, $directories, $excludeDirs, $namePatterns);
+
+ $this->cache->set($cacheKey, $discoveryState);
+
+ return $discoveryState;
+ }
+
+ /**
+ * Generate a cache key based on discovery parameters.
+ *
+ * @param array $directories
+ * @param array $excludeDirs
+ */
+ private function generateCacheKey(string $basePath, array $directories, array $excludeDirs): string
+ {
+ $keyData = [
+ 'base_path' => $basePath,
+ 'directories' => $directories,
+ 'exclude_dirs' => $excludeDirs,
+ ];
+
+ return self::CACHE_PREFIX.md5(serialize($keyData));
+ }
+
+ /**
+ * Clear the discovery cache.
+ * Useful for development or when files change.
+ */
+ public function clearCache(): void
+ {
+ $this->cache->clear();
+ $this->logger->info('Discovery cache cleared');
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/Discoverer.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/Discoverer.php
new file mode 100644
index 0000000..5b7e476
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/Discoverer.php
@@ -0,0 +1,451 @@
+
+ */
+final class Discoverer implements DiscovererInterface
+{
+ public function __construct(
+ private readonly LoggerInterface $logger = new NullLogger(),
+ private ?DocBlockParser $docBlockParser = null,
+ private ?SchemaGeneratorInterface $schemaGenerator = null,
+ ) {
+ if (!class_exists(Finder::class)) {
+ throw new RuntimeException('File-based discovery requires symfony/finder. Run: composer require symfony/finder');
+ }
+
+ $this->docBlockParser = $docBlockParser ?? new DocBlockParser(logger: $this->logger);
+ $this->schemaGenerator = $schemaGenerator ?? new SchemaGenerator($this->docBlockParser);
+ }
+
+ /**
+ * Discover MCP elements in the specified directories and return the discovery state.
+ *
+ * @param string $basePath the base path for resolving directories
+ * @param array $directories list of directories (relative to base path) to scan
+ * @param array $excludeDirs list of directories (relative to base path) to exclude from the scan
+ * @param array $namePatterns list of file name patterns for the scan. Compatible with Finder->name()
+ */
+ public function discover(string $basePath, array $directories, array $excludeDirs = [], array $namePatterns = self::DEFAULT_NAME_PATERNS): DiscoveryState
+ {
+ $startTime = microtime(true);
+ $discoveredCount = [
+ 'tools' => 0,
+ 'resources' => 0,
+ 'prompts' => 0,
+ 'resourceTemplates' => 0,
+ ];
+
+ $namePatterns = !empty($namePatterns) ? $namePatterns : self::DEFAULT_NAME_PATERNS;
+
+ $tools = [];
+ $resources = [];
+ $prompts = [];
+ $resourceTemplates = [];
+
+ try {
+ $finder = new Finder();
+ $absolutePaths = [];
+ foreach ($directories as $dir) {
+ $path = rtrim($basePath, '/').'/'.ltrim($dir, '/');
+ if (is_dir($path)) {
+ $absolutePaths[] = $path;
+ }
+ }
+
+ if (empty($absolutePaths)) {
+ $this->logger->warning('No valid discovery directories found to scan.', [
+ 'configured_paths' => $directories,
+ 'base_path' => $basePath,
+ ]);
+
+ return new DiscoveryState();
+ }
+
+ $finder->files()
+ ->in($absolutePaths)
+ ->exclude($excludeDirs)
+ ->name($namePatterns);
+
+ foreach ($finder as $file) {
+ $this->processFile($file, $discoveredCount, $tools, $resources, $prompts, $resourceTemplates);
+ }
+ } catch (\Throwable $e) {
+ $this->logger->error('Error during file finding process for MCP discovery'.json_encode($e->getTrace(), \JSON_PRETTY_PRINT), [
+ 'exception' => $e,
+ ]);
+ }
+
+ $duration = microtime(true) - $startTime;
+ $this->logger->info('Attribute discovery finished.', [
+ 'duration_sec' => round($duration, 3),
+ 'tools' => $discoveredCount['tools'],
+ 'resources' => $discoveredCount['resources'],
+ 'prompts' => $discoveredCount['prompts'],
+ 'resourceTemplates' => $discoveredCount['resourceTemplates'],
+ ]);
+
+ return new DiscoveryState($tools, $resources, $prompts, $resourceTemplates);
+ }
+
+ /**
+ * Process a single PHP file for MCP elements on classes or methods.
+ *
+ * @param DiscoveredCount $discoveredCount
+ * @param array $tools
+ * @param array $resources
+ * @param array $prompts
+ * @param array $resourceTemplates
+ */
+ private function processFile(SplFileInfo $file, array &$discoveredCount, array &$tools, array &$resources, array &$prompts, array &$resourceTemplates): void
+ {
+ $className = $this->getClassFromFile($file);
+ if (!$className) {
+ $this->logger->warning('No valid class found in file', ['file' => $file->getPathname()]);
+
+ return;
+ }
+
+ try {
+ $reflectionClass = new \ReflectionClass($className);
+
+ if ($reflectionClass->isAbstract() || $reflectionClass->isInterface() || $reflectionClass->isTrait() || $reflectionClass->isEnum()) {
+ return;
+ }
+
+ $processedViaClassAttribute = false;
+ if ($reflectionClass->hasMethod('__invoke')) {
+ $invokeMethod = $reflectionClass->getMethod('__invoke');
+ if ($invokeMethod->isPublic() && !$invokeMethod->isStatic()) {
+ $attributeTypes = [McpTool::class, McpResource::class, McpPrompt::class, McpResourceTemplate::class];
+ foreach ($attributeTypes as $attributeType) {
+ $classAttribute = $reflectionClass->getAttributes($attributeType, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
+ if ($classAttribute) {
+ $this->processMethod($invokeMethod, $discoveredCount, $classAttribute, $tools, $resources, $prompts, $resourceTemplates);
+ $processedViaClassAttribute = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!$processedViaClassAttribute) {
+ foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
+ if (
+ $method->getDeclaringClass()->getName() !== $reflectionClass->getName()
+ || $method->isStatic() || $method->isAbstract() || $method->isConstructor() || $method->isDestructor() || '__invoke' === $method->getName()
+ ) {
+ continue;
+ }
+ $attributeTypes = [McpTool::class, McpResource::class, McpPrompt::class, McpResourceTemplate::class];
+ foreach ($attributeTypes as $attributeType) {
+ $methodAttribute = $method->getAttributes($attributeType, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null;
+ if ($methodAttribute) {
+ $this->processMethod($method, $discoveredCount, $methodAttribute, $tools, $resources, $prompts, $resourceTemplates);
+ break;
+ }
+ }
+ }
+ }
+ } catch (\ReflectionException $e) {
+ $this->logger->error('Reflection error processing file for MCP discovery', ['file' => $file->getPathname(), 'class' => $className, 'exception' => $e]);
+ } catch (\Throwable $e) {
+ $this->logger->error('Unexpected error processing file for MCP discovery', [
+ 'file' => $file->getPathname(),
+ 'class' => $className,
+ 'exception' => $e,
+ ]);
+ }
+ }
+
+ /**
+ * Process a method with a given MCP attribute instance.
+ * Can be called for regular methods or the __invoke method of an invokable class.
+ *
+ * @param \ReflectionMethod $method The target method (e.g., regular method or __invoke).
+ * @param DiscoveredCount $discoveredCount pass by reference to update counts
+ * @param \ReflectionAttribute $attribute the ReflectionAttribute instance found (on method or class)
+ * @param array $tools
+ * @param array $resources
+ * @param array $prompts
+ * @param array $resourceTemplates
+ */
+ private function processMethod(\ReflectionMethod $method, array &$discoveredCount, \ReflectionAttribute $attribute, array &$tools, array &$resources, array &$prompts, array &$resourceTemplates): void
+ {
+ $className = $method->getDeclaringClass()->getName();
+ $methodName = $method->getName();
+ $attributeClassName = $attribute->getName();
+
+ try {
+ $instance = $attribute->newInstance();
+
+ switch ($attributeClassName) {
+ case McpTool::class:
+ $name = ElementMetadataResolver::resolveName($method, $instance->name);
+ $description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
+ $inputSchema = $this->schemaGenerator->generate($method);
+ $outputSchema = $this->schemaGenerator->generateOutputSchema($method);
+ $tool = new Tool(
+ name: $name,
+ title: $instance->title,
+ inputSchema: $inputSchema,
+ description: $description,
+ annotations: $instance->annotations,
+ icons: $instance->icons,
+ meta: $instance->meta,
+ outputSchema: $outputSchema,
+ );
+ $tools[$name] = new ToolReference($tool, [$className, $methodName]);
+ ++$discoveredCount['tools'];
+ break;
+
+ case McpResource::class:
+ $name = ElementMetadataResolver::resolveName($method, $instance->name);
+ $description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
+ $resource = new ResourceDefinition(
+ $instance->uri,
+ $name,
+ $instance->title,
+ $description,
+ $instance->mimeType,
+ $instance->annotations,
+ $instance->size,
+ $instance->icons,
+ $instance->meta,
+ );
+ $resources[$instance->uri] = new ResourceReference($resource, [$className, $methodName]);
+
+ ++$discoveredCount['resources'];
+ break;
+
+ case McpPrompt::class:
+ $docBlock = $this->docBlockParser->parseDocBlock($method->getDocComment() ?? null);
+ $name = ElementMetadataResolver::resolveName($method, $instance->name);
+ $description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
+ $arguments = [];
+ $paramTags = $this->docBlockParser->getParamTags($docBlock);
+ foreach ($method->getParameters() as $param) {
+ $reflectionType = $param->getType();
+ if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
+ continue;
+ }
+ $paramTag = $paramTags['$'.$param->getName()] ?? null;
+ $arguments[] = new PromptArgument($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, !$param->isOptional() && !$param->isDefaultValueAvailable());
+ }
+ $prompt = new Prompt($name, $instance->title, $description, $arguments, $instance->icons, $instance->meta);
+ $completionProviders = $this->getCompletionProviders($method);
+ $prompts[$name] = new PromptReference($prompt, [$className, $methodName], $completionProviders);
+ ++$discoveredCount['prompts'];
+ break;
+
+ case McpResourceTemplate::class:
+ $name = ElementMetadataResolver::resolveName($method, $instance->name);
+ $description = ElementMetadataResolver::resolveDescription($method, $instance->description, $this->docBlockParser);
+ $mimeType = $instance->mimeType;
+ $annotations = $instance->annotations;
+ $meta = $instance->meta ?? null;
+ $resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $instance->title, $description, $mimeType, $annotations, $meta);
+ $completionProviders = $this->getCompletionProviders($method);
+ $resourceTemplates[$instance->uriTemplate] = new ResourceTemplateReference($resourceTemplate, [$className, $methodName], $completionProviders);
+ ++$discoveredCount['resourceTemplates'];
+ break;
+ }
+ } catch (ExceptionInterface $e) {
+ $this->logger->error("Failed to process MCP attribute on {$className}::{$methodName}", [
+ 'attribute' => $attributeClassName,
+ 'exception' => $e,
+ ]);
+ } catch (\Throwable $e) {
+ $this->logger->error("Unexpected error processing attribute on {$className}::{$methodName}", [
+ 'attribute' => $attributeClassName,
+ 'exception' => $e,
+ ]);
+ }
+ }
+
+ /**
+ * @return array
+ */
+ private function getCompletionProviders(\ReflectionMethod $reflectionMethod): array
+ {
+ $completionProviders = [];
+ foreach ($reflectionMethod->getParameters() as $param) {
+ $reflectionType = $param->getType();
+ if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
+ continue;
+ }
+
+ $completionAttributes = $param->getAttributes(CompletionProvider::class, \ReflectionAttribute::IS_INSTANCEOF);
+ if (!empty($completionAttributes)) {
+ $attributeInstance = $completionAttributes[0]->newInstance();
+
+ if ($attributeInstance->provider) {
+ $completionProviders[$param->getName()] = $attributeInstance->provider;
+ } elseif ($attributeInstance->providerClass) {
+ $completionProviders[$param->getName()] = $attributeInstance->provider;
+ } elseif ($attributeInstance->values) {
+ $completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
+ } elseif ($attributeInstance->enum) {
+ $completionProviders[$param->getName()] = new EnumCompletionProvider($attributeInstance->enum);
+ }
+ }
+ }
+
+ return $completionProviders;
+ }
+
+ /**
+ * Attempt to determine the FQCN from a PHP file path.
+ * Uses tokenization to extract namespace and class name.
+ *
+ * @return class-string|null the FQCN or null if not found/determinable
+ */
+ private function getClassFromFile(SplFileInfo $file): ?string
+ {
+ $this->logger->debug('Processing file', ['path' => $file->getPathname()]);
+
+ try {
+ $content = $file->getContents();
+ } catch (\Throwable $e) {
+ $this->logger->warning("Failed to read file content during class discovery: {$file->getPathname()}", [
+ 'exception' => $e,
+ ]);
+
+ return null;
+ }
+
+ if (\strlen($content) > 500 * 1024) {
+ $this->logger->warning('Skipping large file during class discovery.', ['file' => $file->getPathname()]);
+
+ return null;
+ }
+
+ try {
+ $tokens = token_get_all($content);
+ } catch (\Throwable $e) {
+ $this->logger->warning("Failed to tokenize file during class discovery: {$file->getPathname()}", [
+ 'exception' => $e,
+ ]);
+
+ return null;
+ }
+
+ $namespace = '';
+ $namespaceFound = false;
+ $level = 0;
+ $potentialClasses = [];
+
+ $tokenCount = \count($tokens);
+ for ($i = 0; $i < $tokenCount; ++$i) {
+ if (\is_array($tokens[$i]) && \T_NAMESPACE === $tokens[$i][0]) {
+ $namespace = '';
+ for ($j = $i + 1; $j < $tokenCount; ++$j) {
+ if (';' === $tokens[$j] || '{' === $tokens[$j]) {
+ $namespaceFound = true;
+ $i = $j;
+ break;
+ }
+ if (\is_array($tokens[$j]) && \in_array($tokens[$j][0], [\T_STRING, \T_NAME_QUALIFIED])) {
+ $namespace .= $tokens[$j][1];
+ } elseif (\T_NS_SEPARATOR === $tokens[$j][0]) {
+ $namespace .= '\\';
+ }
+ }
+ if ($namespaceFound) {
+ break;
+ }
+ }
+ }
+ $namespace = trim($namespace, '\\');
+
+ for ($i = 0; $i < $tokenCount; ++$i) {
+ $token = $tokens[$i];
+ if ('{' === $token) {
+ ++$level;
+
+ continue;
+ }
+ if ('}' === $token) {
+ --$level;
+
+ continue;
+ }
+
+ if ($level === ($namespaceFound && str_contains($content, "namespace {$namespace} {") ? 1 : 0)) {
+ if (\is_array($token) && \in_array($token[0], [\T_CLASS, \T_INTERFACE, \T_TRAIT, \defined('T_ENUM') ? \T_ENUM : -1])) {
+ for ($j = $i + 1; $j < $tokenCount; ++$j) {
+ if (\is_array($tokens[$j]) && \T_STRING === $tokens[$j][0]) {
+ $className = $tokens[$j][1];
+ $potentialClasses[] = $namespace ? $namespace.'\\'.$className : $className;
+ $i = $j;
+ break;
+ }
+ if (';' === $tokens[$j] || '{' === $tokens[$j] || ')' === $tokens[$j]) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ foreach ($potentialClasses as $potentialClass) {
+ if (class_exists($potentialClass, true)) {
+ return $potentialClass;
+ }
+ }
+
+ if (!empty($potentialClasses)) {
+ if (!class_exists($potentialClasses[0], false)) {
+ $this->logger->debug('getClassFromFile returning potential non-class type. Are you sure this class has been autoloaded?', ['file' => $file->getPathname(), 'type' => $potentialClasses[0]]);
+ }
+
+ return $potentialClasses[0];
+ }
+
+ return null;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/DiscovererInterface.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/DiscovererInterface.php
new file mode 100644
index 0000000..2df69f3
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/DiscovererInterface.php
@@ -0,0 +1,34 @@
+
+ */
+interface DiscovererInterface
+{
+ public const DEFAULT_NAME_PATERNS = ['*.php'];
+
+ /**
+ * Discover MCP elements in the specified directories and return the discovery state.
+ *
+ * @param string $basePath the base path for resolving directories
+ * @param array $directories list of directories (relative to base path) to scan
+ * @param array $excludeDirs list of directories (relative to base path) to exclude from the scan
+ * @param array $namePatterns list of file name patterns for the scan. Compatible with Finder->name()
+ */
+ public function discover(string $basePath, array $directories, array $excludeDirs = [], array $namePatterns = self::DEFAULT_NAME_PATERNS): DiscoveryState;
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/DiscoveryState.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/DiscoveryState.php
new file mode 100644
index 0000000..59a73ec
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/DiscoveryState.php
@@ -0,0 +1,128 @@
+
+ */
+final class DiscoveryState
+{
+ /**
+ * @param array $tools
+ * @param array $resources
+ * @param array $prompts
+ * @param array $resourceTemplates
+ */
+ public function __construct(
+ private readonly array $tools = [],
+ private readonly array $resources = [],
+ private readonly array $prompts = [],
+ private readonly array $resourceTemplates = [],
+ ) {
+ }
+
+ /**
+ * @return array
+ */
+ public function getTools(): array
+ {
+ return $this->tools;
+ }
+
+ /**
+ * @return array
+ */
+ public function getResources(): array
+ {
+ return $this->resources;
+ }
+
+ /**
+ * @return array
+ */
+ public function getPrompts(): array
+ {
+ return $this->prompts;
+ }
+
+ /**
+ * @return array
+ */
+ public function getResourceTemplates(): array
+ {
+ return $this->resourceTemplates;
+ }
+
+ /**
+ * Returns the subset of this state whose keys are absent from $next.
+ *
+ * Asymmetric by design: entries whose keys exist in both states are excluded
+ * regardless of value. Used to identify owned entries that a fresh discovery
+ * no longer produces.
+ */
+ public function obsoletedBy(self $next): self
+ {
+ return new self(
+ array_diff_key($this->tools, $next->tools),
+ array_diff_key($this->resources, $next->resources),
+ array_diff_key($this->prompts, $next->prompts),
+ array_diff_key($this->resourceTemplates, $next->resourceTemplates),
+ );
+ }
+
+ /**
+ * Check if this state contains any discovered elements.
+ */
+ public function isEmpty(): bool
+ {
+ return empty($this->tools)
+ && empty($this->resources)
+ && empty($this->prompts)
+ && empty($this->resourceTemplates);
+ }
+
+ /**
+ * Get the total count of discovered elements.
+ */
+ public function getElementCount(): int
+ {
+ return \count($this->tools)
+ + \count($this->resources)
+ + \count($this->prompts)
+ + \count($this->resourceTemplates);
+ }
+
+ /**
+ * Get a breakdown of discovered elements by type.
+ *
+ * @return array{tools: int, resources: int, prompts: int, resourceTemplates: int}
+ */
+ public function getElementCounts(): array
+ {
+ return [
+ 'tools' => \count($this->tools),
+ 'resources' => \count($this->resources),
+ 'prompts' => \count($this->prompts),
+ 'resourceTemplates' => \count($this->resourceTemplates),
+ ];
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/DocBlockParser.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/DocBlockParser.php
new file mode 100644
index 0000000..eb14a14
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/DocBlockParser.php
@@ -0,0 +1,125 @@
+
+ */
+class DocBlockParser
+{
+ private DocBlockFactoryInterface $docBlockFactory;
+
+ public function __construct(
+ ?DocBlockFactoryInterface $docBlockFactory = null,
+ private readonly LoggerInterface $logger = new NullLogger(),
+ ) {
+ $this->docBlockFactory = $docBlockFactory ?? DocBlockFactory::createInstance();
+ }
+
+ /**
+ * Safely parses a DocComment string into a DocBlock object.
+ */
+ public function parseDocBlock(string|false|null $docComment): ?DocBlock
+ {
+ if (false === $docComment || null === $docComment || empty($docComment)) {
+ return null;
+ }
+ try {
+ return $this->docBlockFactory->create($docComment);
+ } catch (\Throwable $e) {
+ // Log error or handle gracefully if invalid DocBlock syntax is encountered
+ $this->logger->warning('Failed to parse DocBlock', [
+ 'exception' => $e,
+ ]);
+
+ return null;
+ }
+ }
+
+ /**
+ * Gets the description from a DocBlock (summary + description body).
+ */
+ public function getDescription(?DocBlock $docBlock): ?string
+ {
+ if (!$docBlock) {
+ return null;
+ }
+ $summary = trim($docBlock->getSummary());
+ $descriptionBody = trim((string) $docBlock->getDescription());
+
+ if ($summary && $descriptionBody) {
+ return $summary."\n\n".$descriptionBody;
+ }
+ if ($summary) {
+ return $summary;
+ }
+ if ($descriptionBody) {
+ return $descriptionBody;
+ }
+
+ return null;
+ }
+
+ /**
+ * Extracts "@param" tag information from a DocBlock, keyed by variable name (e.g., '$paramName').
+ *
+ * @return array
+ */
+ public function getParamTags(?DocBlock $docBlock): array
+ {
+ if (!$docBlock) {
+ return [];
+ }
+
+ /** @var array $paramTags */
+ $paramTags = [];
+ foreach ($docBlock->getTagsByName('param') as $tag) {
+ if ($tag instanceof Param && $tag->getVariableName()) {
+ $paramTags['$'.$tag->getVariableName()] = $tag;
+ }
+ }
+
+ return $paramTags;
+ }
+
+ /**
+ * Gets the description string from a Param tag.
+ */
+ public function getParamDescription(?Param $paramTag): ?string
+ {
+ return $paramTag ? (trim((string) $paramTag->getDescription()) ?: null) : null;
+ }
+
+ /**
+ * Gets the type string from a Param tag.
+ */
+ public function getParamTypeString(?Param $paramTag): ?string
+ {
+ if ($paramTag && $paramTag->getType()) {
+ $typeFromTag = trim((string) $paramTag->getType());
+ if (!empty($typeFromTag)) {
+ return ltrim($typeFromTag, '\\');
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/ElementMetadataResolver.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/ElementMetadataResolver.php
new file mode 100644
index 0000000..3c755dd
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/ElementMetadataResolver.php
@@ -0,0 +1,44 @@
+
+ */
+final class ElementMetadataResolver
+{
+ private function __construct()
+ {
+ }
+
+ /**
+ * Explicit names win; otherwise invokable classes are named after the class,
+ * regular handlers after the method.
+ */
+ public static function resolveName(\ReflectionMethod $method, ?string $name): string
+ {
+ $methodName = $method->getName();
+
+ return $name ?? ('__invoke' === $methodName ? $method->getDeclaringClass()->getShortName() : $methodName);
+ }
+
+ /**
+ * Explicit descriptions win; otherwise the summary of the method's doc block is used.
+ */
+ public static function resolveDescription(\ReflectionMethod $method, ?string $description, DocBlockParser $docBlockParser): ?string
+ {
+ return $description ?? $docBlockParser->getDescription($docBlockParser->parseDocBlock($method->getDocComment() ?? null));
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/HandlerResolver.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/HandlerResolver.php
new file mode 100644
index 0000000..8cadcd3
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/HandlerResolver.php
@@ -0,0 +1,96 @@
+
+ */
+class HandlerResolver
+{
+ /**
+ * Validates and resolves a handler to a ReflectionMethod or ReflectionFunction instance.
+ *
+ * A handler can be:
+ * - A Closure: function() { ... }
+ * - An array: [ClassName::class, 'methodName'] (resolved on a new or container-provided instance)
+ * - An array: [$instance, 'methodName'] (method on a pre-built object instance)
+ * - An array: [ClassName::class, 'staticMethod'] (static method, if callable)
+ * - A string: InvokableClassName::class (which will resolve to its '__invoke' method)
+ *
+ * @param Handler $handler the handler to resolve
+ *
+ * @throws InvalidArgumentException If the handler format is invalid, the class/method doesn't exist,
+ * or the method is unsuitable (e.g., private, abstract).
+ */
+ public static function resolve(\Closure|array|string $handler): \ReflectionMethod|\ReflectionFunction
+ {
+ if ($handler instanceof \Closure) {
+ return new \ReflectionFunction($handler);
+ }
+
+ if (\is_array($handler)) {
+ // A Closure in slot 0 must fall through to the format error rather than
+ // be treated as an instance, where "$closure::class" would yield the
+ // misleading class name "Closure".
+ $target = $handler[0] ?? null;
+ $hasValidTarget = (\is_string($target) || \is_object($target)) && !$target instanceof \Closure;
+
+ if (2 !== \count($handler) || !$hasValidTarget || !isset($handler[1]) || !\is_string($handler[1])) {
+ throw new InvalidArgumentException('Invalid array handler format. Expected [ClassName::class, \'methodName\'] or [$instance, \'methodName\'].');
+ }
+ [$classOrObject, $methodName] = $handler;
+ $className = \is_object($classOrObject) ? $classOrObject::class : $classOrObject;
+ if (!class_exists($className)) {
+ throw new InvalidArgumentException(\sprintf('Handler class "%s" not found for array handler.', $className));
+ }
+ if (!method_exists($className, $methodName)) {
+ throw new InvalidArgumentException(\sprintf('Handler method "%s" not found in class "%s" for array handler.', $methodName, $className));
+ }
+ } elseif (class_exists($handler)) {
+ $className = $handler;
+ $methodName = '__invoke';
+ if (!method_exists($className, $methodName)) {
+ throw new InvalidArgumentException(\sprintf('Invokable handler class "%s" must have a public "__invoke" method.', $className));
+ }
+ } else {
+ throw new InvalidArgumentException('Invalid handler format. Expected Closure, [ClassName::class, \'methodName\'] or InvokableClassName::class string.');
+ }
+
+ try {
+ $reflectionMethod = new \ReflectionMethod($className, $methodName);
+
+ // For discovered elements (non-manual), still reject static methods
+ // For manual elements, we'll allow static methods since they're callable
+ if (!$reflectionMethod->isPublic()) {
+ throw new InvalidArgumentException(\sprintf('Handler method "%s::%s" must be public.', $className, $methodName));
+ }
+ if ($reflectionMethod->isAbstract()) {
+ throw new InvalidArgumentException(\sprintf('Handler method "%s::%s" must not be abstract.', $className, $methodName));
+ }
+ if ($reflectionMethod->isConstructor() || $reflectionMethod->isDestructor()) {
+ throw new InvalidArgumentException(\sprintf('Handler method "%s::%s" cannot be a constructor or destructor.', $className, $methodName));
+ }
+
+ return $reflectionMethod;
+ } catch (\ReflectionException $e) {
+ // This typically occurs if class_exists passed but ReflectionMethod still fails (rare)
+ throw new InvalidArgumentException(\sprintf('Reflection error for handler "%s::%s": %s', $className, $methodName, $e->getMessage()), 0, $e);
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/SchemaGenerator.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/SchemaGenerator.php
new file mode 100644
index 0000000..357c9e9
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/SchemaGenerator.php
@@ -0,0 +1,964 @@
+
+ * }
+ * @phpstan-type InferredParameterSchema array{
+ * type?: string|array,
+ * description?: string,
+ * default?: mixed,
+ * enum?: array,
+ * items?: array,
+ * }
+ * @phpstan-type VariadicParameterSchema array{
+ * type: 'array',
+ * items?: array,
+ * description?: string,
+ * parameter_schema?: array
+ * }
+ *
+ * @author Kyrian Obikwelu
+ */
+final class SchemaGenerator implements SchemaGeneratorInterface
+{
+ public function __construct(
+ private readonly DocBlockParser $docBlockParser,
+ ) {
+ }
+
+ /**
+ * Generates a JSON Schema object (as a PHP array) for parameters.
+ *
+ * @return array
+ */
+ public function generate(\Reflector $reflection): array
+ {
+ if ($reflection instanceof \ReflectionClass) {
+ throw new BadMethodCallException('Schema generation from ReflectionClass is not implemented yet. Use ReflectionMethod or ReflectionFunction instead.');
+ }
+
+ if (!$reflection instanceof \ReflectionMethod && !$reflection instanceof \ReflectionFunction) {
+ throw new BadMethodCallException(\sprintf('Schema generation from %s is not supported. Use ReflectionMethod or ReflectionFunction instead.', $reflection::class));
+ }
+
+ $methodSchema = $this->extractMethodLevelSchema($reflection);
+
+ if ($methodSchema && isset($methodSchema['definition'])) {
+ return $methodSchema['definition'];
+ }
+
+ $parametersInfo = $this->parseParametersInfo($reflection);
+
+ return $this->buildSchemaFromParameters($parametersInfo, $methodSchema);
+ }
+
+ /**
+ * Generates a JSON Schema object (as a PHP array) for a method's or function's return type.
+ *
+ * Only returns an outputSchema if explicitly provided in the McpTool attribute.
+ * Per MCP spec, outputSchema should only be present when explicitly provided.
+ *
+ * @return ?array
+ */
+ public function generateOutputSchema(\Reflector $reflection): ?array
+ {
+ if ($reflection instanceof \ReflectionClass) {
+ throw new BadMethodCallException('Schema generation from ReflectionClass is not implemented yet. Use ReflectionMethod or ReflectionFunction instead.');
+ }
+
+ if (!$reflection instanceof \ReflectionMethod && !$reflection instanceof \ReflectionFunction) {
+ throw new BadMethodCallException(\sprintf('Schema generation from %s is not supported. Use ReflectionMethod or ReflectionFunction instead.', $reflection::class));
+ }
+
+ // Only return outputSchema if explicitly provided in McpTool attribute
+ $mcpToolAttrs = $reflection->getAttributes(McpTool::class, \ReflectionAttribute::IS_INSTANCEOF);
+ if ($mcpToolAttrs) {
+ $mcpToolInstance = $mcpToolAttrs[0]->newInstance();
+
+ return $mcpToolInstance->outputSchema;
+ }
+
+ return null;
+ }
+
+ /**
+ * Extracts method-level or function-level Schema attribute.
+ *
+ * @return SchemaAttributeData
+ */
+ private function extractMethodLevelSchema(\ReflectionFunctionAbstract $reflection): ?array
+ {
+ $schemaAttrs = $reflection->getAttributes(Schema::class, \ReflectionAttribute::IS_INSTANCEOF);
+ if (empty($schemaAttrs)) {
+ return null;
+ }
+
+ /** @var Schema $schemaAttr */
+ $schemaAttr = $schemaAttrs[0]->newInstance();
+
+ return $schemaAttr->toArray();
+ }
+
+ /**
+ * Extracts parameter-level Schema attribute.
+ *
+ * @return SchemaAttributeData
+ */
+ private function extractParameterLevelSchema(\ReflectionParameter $parameter): array
+ {
+ $schemaAttrs = $parameter->getAttributes(Schema::class, \ReflectionAttribute::IS_INSTANCEOF);
+ if (empty($schemaAttrs)) {
+ return [];
+ }
+
+ /** @var Schema $schemaAttr */
+ $schemaAttr = $schemaAttrs[0]->newInstance();
+
+ return $schemaAttr->toArray();
+ }
+
+ /**
+ * Builds the final schema from parameter information and method-level schema.
+ *
+ * @param ParameterInfo[] $parametersInfo
+ * @param SchemaAttributeData $methodSchema
+ *
+ * @return array
+ */
+ private function buildSchemaFromParameters(array $parametersInfo, ?array $methodSchema): array
+ {
+ $schema = [
+ 'type' => 'object',
+ 'properties' => [],
+ 'required' => [],
+ ];
+
+ // Apply method-level schema as base
+ if ($methodSchema) {
+ $schema = array_merge($schema, $methodSchema);
+ if (!isset($schema['type'])) {
+ $schema['type'] = 'object';
+ }
+ if (!isset($schema['properties'])) {
+ $schema['properties'] = [];
+ }
+ if (!isset($schema['required'])) {
+ $schema['required'] = [];
+ }
+ }
+
+ foreach ($parametersInfo as $paramInfo) {
+ $paramName = $paramInfo['name'];
+
+ $methodLevelParamSchema = $schema['properties'][$paramName] ?? null;
+
+ $paramSchema = $this->buildParameterSchema($paramInfo, $methodLevelParamSchema);
+
+ $schema['properties'][$paramName] = $paramSchema;
+
+ if ($paramInfo['required'] && !\in_array($paramName, $schema['required'])) {
+ $schema['required'][] = $paramName;
+ } elseif (!$paramInfo['required'] && ($key = array_search($paramName, $schema['required'])) !== false) {
+ unset($schema['required'][$key]);
+ $schema['required'] = array_values($schema['required']); // Re-index
+ }
+ }
+
+ // Clean up empty properties
+ if (empty($schema['properties'])) {
+ $schema['properties'] = new \stdClass();
+ }
+ if (empty($schema['required'])) {
+ unset($schema['required']);
+ }
+
+ return $schema;
+ }
+
+ /**
+ * Builds the final schema for a single parameter by merging all three levels.
+ *
+ * @param ParameterInfo $paramInfo
+ * @param array|null $methodLevelParamSchema
+ */
+ private function buildParameterSchema(array $paramInfo, ?array $methodLevelParamSchema): array
+ {
+ if ($paramInfo['is_variadic']) {
+ return $this->ensureArrayItems($this->buildVariadicParameterSchema($paramInfo));
+ }
+
+ $inferredSchema = $this->buildInferredParameterSchema($paramInfo);
+
+ // Method-level takes precedence over inferred schema
+ $mergedSchema = $inferredSchema;
+ if ($methodLevelParamSchema) {
+ $mergedSchema = array_merge($inferredSchema, $methodLevelParamSchema);
+ }
+
+ // Parameter-level takes highest precedence
+ $parameterLevelSchema = $paramInfo['parameter_schema'];
+ if (!empty($parameterLevelSchema)) {
+ // A complete definition replaces the schema, as it does at method level.
+ if (isset($parameterLevelSchema['definition']) && \is_array($parameterLevelSchema['definition'])) {
+ $mergedSchema = $parameterLevelSchema['definition'];
+
+ // The default comes from the signature, not from the schema.
+ if (!\array_key_exists('default', $mergedSchema) && \array_key_exists('default', $inferredSchema)) {
+ $mergedSchema['default'] = $inferredSchema['default'];
+ }
+ } else {
+ $mergedSchema = array_merge($mergedSchema, $parameterLevelSchema);
+ }
+ }
+
+ // Run after all merges so that when a Schema attribute reshapes the parameter
+ // (e.g. to `object`), the array `items` invariant is only enforced on what is
+ // genuinely still an array.
+ return $this->ensureArrayItems($mergedSchema);
+ }
+
+ /**
+ * Guarantees an array-typed schema always declares `items`.
+ *
+ * `items` is optional in JSON Schema, but some strict clients reject an array schema
+ * without it. When no element type could be inferred, default to the empty schema `{}`
+ * (matches anything) — represented as `new \stdClass()` so it serializes to `{}` rather
+ * than `[]`.
+ *
+ * @param array $schema
+ *
+ * @return array
+ */
+ private function ensureArrayItems(array $schema): array
+ {
+ $type = $schema['type'] ?? null;
+ $isArray = 'array' === $type || (\is_array($type) && \in_array('array', $type, true));
+
+ if ($isArray && !isset($schema['items'])) {
+ $schema['items'] = new \stdClass();
+ }
+
+ return $schema;
+ }
+
+ /**
+ * Builds parameter schema from inferred type and docblock information only.
+ * Returns empty array for variadic parameters (handled separately).
+ *
+ * @param ParameterInfo $paramInfo
+ *
+ * @return InferredParameterSchema
+ */
+ private function buildInferredParameterSchema(array $paramInfo): array
+ {
+ $paramSchema = [];
+
+ // Variadic parameters are handled separately
+ if ($paramInfo['is_variadic']) {
+ return [];
+ }
+
+ // Infer JSON Schema types
+ $jsonTypes = $this->inferParameterTypes($paramInfo);
+
+ if (1 === \count($jsonTypes)) {
+ $paramSchema['type'] = $jsonTypes[0];
+ } elseif (\count($jsonTypes) > 1) {
+ $paramSchema['type'] = $jsonTypes;
+ }
+
+ // Add description from docblock
+ if ($paramInfo['description']) {
+ $paramSchema['description'] = $paramInfo['description'];
+ }
+
+ // Add default value only if parameter actually has a default
+ if ($paramInfo['has_default']) {
+ $paramSchema['default'] = $paramInfo['default_value'];
+ }
+
+ // Handle enums
+ $paramSchema = $this->applyEnumConstraints($paramSchema, $paramInfo);
+
+ // Handle array items
+ return $this->applyArrayConstraints($paramSchema, $paramInfo);
+ }
+
+ /**
+ * Builds schema for variadic parameters.
+ *
+ * @param ParameterInfo $paramInfo
+ *
+ * @return VariadicParameterSchema
+ */
+ private function buildVariadicParameterSchema(array $paramInfo): array
+ {
+ $paramSchema = ['type' => 'array'];
+
+ // Add description from docblock before parameter-level schema is merged in,
+ // so a definition's own description takes precedence (matches buildParameterSchema).
+ if ($paramInfo['description']) {
+ $paramSchema['description'] = $paramInfo['description'];
+ }
+
+ // Apply parameter-level Schema attributes first
+ if (!empty($paramInfo['parameter_schema'])) {
+ $parameterLevelSchema = $paramInfo['parameter_schema'];
+
+ $paramSchema = isset($parameterLevelSchema['definition']) && \is_array($parameterLevelSchema['definition'])
+ ? $parameterLevelSchema['definition']
+ : array_merge($paramSchema, $parameterLevelSchema);
+
+ // Ensure type is always array for variadic
+ $paramSchema['type'] = 'array';
+ }
+
+ // If no items specified by Schema attribute, infer from type
+ if (!isset($paramSchema['items'])) {
+ $itemJsonTypes = $this->mapPhpTypeToJsonSchemaType($paramInfo['type_string']);
+ $nonNullItemTypes = array_filter($itemJsonTypes, static fn ($t) => 'null' !== $t);
+
+ if (1 === \count($nonNullItemTypes)) {
+ $paramSchema['items'] = ['type' => $nonNullItemTypes[0]];
+ }
+ }
+
+ return $paramSchema;
+ }
+
+ /**
+ * Infers JSON Schema types for a parameter.
+ *
+ * @param ParameterInfo $paramInfo
+ */
+ private function inferParameterTypes(array $paramInfo): array
+ {
+ $jsonTypes = $this->mapPhpTypeToJsonSchemaType($paramInfo['type_string']);
+
+ if ($paramInfo['allows_null'] && 'mixed' !== strtolower($paramInfo['type_string']) && !\in_array('null', $jsonTypes)) {
+ $jsonTypes[] = 'null';
+ }
+
+ if (\count($jsonTypes) > 1) {
+ // Sort but ensure null comes first for consistency
+ $nullIndex = array_search('null', $jsonTypes);
+ if (false !== $nullIndex) {
+ unset($jsonTypes[$nullIndex]);
+ sort($jsonTypes);
+ array_unshift($jsonTypes, 'null');
+ } else {
+ sort($jsonTypes);
+ }
+ }
+
+ return $jsonTypes;
+ }
+
+ /**
+ * Applies enum constraints to parameter schema.
+ */
+ private function applyEnumConstraints(array $paramSchema, array $paramInfo): array
+ {
+ $reflectionType = $paramInfo['reflection_type_object'];
+
+ if (!($reflectionType instanceof \ReflectionNamedType) || $reflectionType->isBuiltin() || !enum_exists($reflectionType->getName())) {
+ return $paramSchema;
+ }
+
+ $enumClass = $reflectionType->getName();
+ $enumReflection = new \ReflectionEnum($enumClass);
+ $backingTypeReflection = $enumReflection->getBackingType();
+
+ if ($enumReflection->isBacked() && $backingTypeReflection instanceof \ReflectionNamedType) {
+ $paramSchema['enum'] = array_column($enumClass::cases(), 'value');
+ $jsonBackingType = match ($backingTypeReflection->getName()) {
+ 'int' => 'integer',
+ 'string' => 'string',
+ default => null,
+ };
+
+ if ($jsonBackingType) {
+ if (isset($paramSchema['type']) && \is_array($paramSchema['type']) && \in_array('null', $paramSchema['type'])) {
+ $paramSchema['type'] = [$jsonBackingType, 'null'];
+ $paramSchema['enum'][] = null;
+ } else {
+ $paramSchema['type'] = $jsonBackingType;
+ }
+ }
+ } else {
+ // Non-backed enum - use names as enum values
+ $paramSchema['enum'] = array_column($enumClass::cases(), 'name');
+ if (isset($paramSchema['type']) && \is_array($paramSchema['type']) && \in_array('null', $paramSchema['type'])) {
+ $paramSchema['type'] = ['string', 'null'];
+ $paramSchema['enum'][] = null;
+ } else {
+ $paramSchema['type'] = 'string';
+ }
+ }
+
+ return $paramSchema;
+ }
+
+ /**
+ * Applies array-specific constraints to parameter schema.
+ */
+ private function applyArrayConstraints(array $paramSchema, array $paramInfo): array
+ {
+ if (!isset($paramSchema['type'])) {
+ return $paramSchema;
+ }
+
+ $typeString = $paramInfo['type_string'];
+ $allowsNull = $paramInfo['allows_null'];
+
+ // Handle object-like arrays using array{} syntax
+ if (preg_match('/^array\s*{/i', $typeString)) {
+ $objectSchema = $this->inferArrayItemsType($typeString);
+ if (\is_array($objectSchema) && isset($objectSchema['properties'])) {
+ $paramSchema = array_merge($paramSchema, $objectSchema);
+ $paramSchema['type'] = $allowsNull ? ['object', 'null'] : 'object';
+ }
+ }
+ // Handle regular arrays
+ elseif (\in_array('array', $this->mapPhpTypeToJsonSchemaType($typeString))) {
+ // `inferArrayItemsType()` expects a single array type, not a union — for
+ // `string[]|int` it would see the whole string, match nothing, and lose
+ // the element-type constraint. Isolate the branch(es) that are arrays.
+ $arrayBranches = array_values(array_filter(
+ self::splitUnion($typeString),
+ fn (string $branch): bool => \in_array('array', $this->mapPhpTypeToJsonSchemaType($branch), true),
+ ));
+ if ([] === $arrayBranches) {
+ $arrayBranches = [$typeString];
+ }
+
+ // Several array branches (`string[]|int[]`) can disagree on their
+ // element type. Applying just one would reject values valid under
+ // the other, so `items` only applies when every array branch agrees.
+ $itemsTypes = array_map($this->inferArrayItemsType(...), $arrayBranches);
+ $itemsType = 1 === \count(array_unique(array_map(json_encode(...), $itemsTypes))) ? $itemsTypes[0] : 'any';
+
+ if ('any' !== $itemsType) {
+ if (\is_string($itemsType)) {
+ $paramSchema['items'] = ['type' => $itemsType];
+ } else {
+ if (!isset($itemsType['type']) && isset($itemsType['properties'])) {
+ $itemsType = array_merge(['type' => 'object'], $itemsType);
+ }
+ $paramSchema['items'] = $itemsType;
+ }
+ }
+
+ // Only when the parameter is an array and nothing else. A union
+ // like `string[]|int` keeps both branches: `items` constrains the
+ // instance only when it *is* an array, so the two coexist and
+ // overwriting the type here would drop the scalar branch —
+ // rejecting a value the handler accepts.
+ $otherTypes = array_values(array_diff(
+ (array) $paramSchema['type'],
+ ['array', 'null'],
+ ));
+
+ if ([] === $otherTypes) {
+ $paramSchema['type'] = $allowsNull ? ['array', 'null'] : 'array';
+
+ if (\is_array($paramSchema['type'])) {
+ sort($paramSchema['type']);
+ }
+ } elseif ($allowsNull && !\in_array('null', (array) $paramSchema['type'], true)) {
+ $paramSchema['type'] = [...(array) $paramSchema['type'], 'null'];
+ sort($paramSchema['type']);
+ }
+ }
+
+ return $paramSchema;
+ }
+
+ /**
+ * Parses detailed information about a method's parameters.
+ *
+ * @return ParameterInfo[]
+ */
+ private function parseParametersInfo(\ReflectionMethod|\ReflectionFunction $reflection): array
+ {
+ $docComment = $reflection->getDocComment() ?: null;
+ $docBlock = $this->docBlockParser->parseDocBlock($docComment);
+ $paramTags = $this->docBlockParser->getParamTags($docBlock);
+ $parametersInfo = [];
+
+ foreach ($reflection->getParameters() as $rp) {
+ $reflectionType = $rp->getType();
+
+ if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()
+ && InjectableParameters::supports($reflectionType->getName())
+ ) {
+ continue;
+ }
+
+ $paramName = $rp->getName();
+ if (\in_array(strtolower($paramName), ['_session', '_request'], true)) {
+ throw new InvalidArgumentException(\sprintf('Handler method "%s::%s" has parameter named "%s" which is not allowed. Please change the name of that parameter.', $reflection->class, $reflection->name, $paramName));
+ }
+ $paramTag = $paramTags['$'.$paramName] ?? null;
+
+ $typeString = $this->getParameterTypeString($rp, $paramTag);
+ $description = $this->docBlockParser->getParamDescription($paramTag);
+ $hasDefault = $rp->isDefaultValueAvailable();
+ $defaultValue = $hasDefault ? $rp->getDefaultValue() : null;
+ $isVariadic = $rp->isVariadic();
+
+ $parameterSchema = $this->extractParameterLevelSchema($rp);
+
+ if ($defaultValue instanceof \BackedEnum) {
+ $defaultValue = $defaultValue->value;
+ }
+
+ if ($defaultValue instanceof \UnitEnum) {
+ $defaultValue = $defaultValue->name;
+ }
+
+ $allowsNull = false;
+ if ($reflectionType && $reflectionType->allowsNull()) {
+ $allowsNull = true;
+ } elseif ($hasDefault && null === $defaultValue) {
+ $allowsNull = true;
+ } elseif (str_contains($typeString, 'null') || 'mixed' === strtolower($typeString)) {
+ $allowsNull = true;
+ }
+
+ $parametersInfo[] = [
+ 'name' => $paramName,
+ 'doc_block_tag' => $paramTag,
+ 'reflection_param' => $rp,
+ 'reflection_type_object' => $reflectionType,
+ 'type_string' => $typeString,
+ 'description' => $description,
+ 'required' => !$rp->isOptional(),
+ 'allows_null' => $allowsNull,
+ 'default_value' => $defaultValue,
+ 'has_default' => $hasDefault,
+ 'is_variadic' => $isVariadic,
+ 'parameter_schema' => $parameterSchema,
+ ];
+ }
+
+ return $parametersInfo;
+ }
+
+ /**
+ * Determines the type string for a parameter, prioritizing DocBlock.
+ */
+ private function getParameterTypeString(\ReflectionParameter $rp, ?Param $paramTag): string
+ {
+ $docBlockType = $this->docBlockParser->getParamTypeString($paramTag);
+ $isDocBlockTypeGeneric = false;
+
+ if (null !== $docBlockType) {
+ if (\in_array(strtolower($docBlockType), ['mixed', 'unknown', ''])) {
+ $isDocBlockTypeGeneric = true;
+ }
+ } else {
+ $isDocBlockTypeGeneric = true; // No tag or no type in tag implies generic
+ }
+
+ $reflectionType = $rp->getType();
+ $reflectionTypeString = null;
+ if ($reflectionType) {
+ $reflectionTypeString = $this->getTypeStringFromReflection($reflectionType, $rp->allowsNull());
+ }
+
+ // Prioritize Reflection if DocBlock type is generic AND Reflection provides a more specific type
+ if ($isDocBlockTypeGeneric && null !== $reflectionTypeString && 'mixed' !== $reflectionTypeString) {
+ return $reflectionTypeString;
+ }
+
+ // Otherwise, use the DocBlock type if it was valid and non-generic
+ if (null !== $docBlockType && !$isDocBlockTypeGeneric) {
+ // Consider if DocBlock adds nullability missing from reflection
+ if (false !== stripos($docBlockType, 'null') && $reflectionTypeString && false === stripos($reflectionTypeString, 'null') && !str_ends_with($reflectionTypeString, '|null')) {
+ // If reflection didn't capture null, but docblock did, append |null (if not already mixed)
+ if ('mixed' !== $reflectionTypeString) {
+ return $reflectionTypeString.'|null';
+ }
+ }
+
+ return $docBlockType;
+ }
+
+ // Fallback to Reflection type even if it was generic ('mixed')
+ if (null !== $reflectionTypeString) {
+ return $reflectionTypeString;
+ }
+
+ // Default to 'mixed' if nothing else found
+ return 'mixed';
+ }
+
+ /**
+ * Converts a ReflectionType object into a type string representation.
+ */
+ private function getTypeStringFromReflection(?\ReflectionType $type, bool $nativeAllowsNull): string
+ {
+ if (null === $type) {
+ return 'mixed';
+ }
+
+ $types = [];
+ if ($type instanceof \ReflectionUnionType) {
+ foreach ($type->getTypes() as $innerType) {
+ $types[] = $this->getTypeStringFromReflection($innerType, $innerType->allowsNull());
+ }
+ if ($nativeAllowsNull) {
+ $types = array_filter($types, static fn ($t) => 'null' !== strtolower($t));
+ }
+ $typeString = implode('|', array_unique(array_filter($types)));
+ } elseif ($type instanceof \ReflectionIntersectionType) {
+ foreach ($type->getTypes() as $innerType) {
+ $types[] = $this->getTypeStringFromReflection($innerType, false);
+ }
+ $typeString = implode('&', array_unique(array_filter($types)));
+ } elseif ($type instanceof \ReflectionNamedType) {
+ $typeString = $type->getName();
+ } else {
+ return 'mixed';
+ }
+
+ $typeString = match (strtolower($typeString)) {
+ 'bool' => 'boolean',
+ 'int' => 'integer',
+ 'float', 'double' => 'number',
+ 'str' => 'string',
+ default => $typeString,
+ };
+
+ $isNullable = $nativeAllowsNull;
+ if ($type instanceof \ReflectionNamedType && 'mixed' === $type->getName()) {
+ $isNullable = true;
+ }
+
+ if ($type instanceof \ReflectionUnionType && !$nativeAllowsNull) {
+ foreach ($type->getTypes() as $innerType) {
+ if ($innerType instanceof \ReflectionNamedType && 'null' === strtolower($innerType->getName())) {
+ $isNullable = true;
+ break;
+ }
+ }
+ }
+
+ if ($isNullable && 'mixed' !== $typeString && false === stripos($typeString, 'null')) {
+ if (!str_ends_with($typeString, '|null') && !str_ends_with($typeString, '&null')) {
+ $typeString .= '|null';
+ }
+ }
+
+ // Remove leading backslash from class names, but handle built-ins like 'int' or unions like 'int|string'
+ if (str_contains($typeString, '\\')) {
+ $parts = preg_split('/([|&])/', $typeString, -1, \PREG_SPLIT_DELIM_CAPTURE);
+ $processedParts = array_map(static fn ($part) => str_starts_with($part, '\\') ? ltrim($part, '\\') : $part, $parts);
+ $typeString = implode('', $processedParts);
+ }
+
+ return $typeString ?: 'mixed';
+ }
+
+ /**
+ * Splits a type string on its top-level `|`, leaving generics and shapes
+ * intact.
+ *
+ * `int|string` splits; `array` and `array{a: int|string}` do
+ * not, because the `|` there is a parameter of the outer type rather than
+ * an alternative to it.
+ *
+ * @return list
+ */
+ public static function splitUnion(string $type): array
+ {
+ $branches = [];
+ $depth = 0;
+ $current = '';
+
+ foreach (str_split($type) as $character) {
+ if (\in_array($character, ['<', '{', '('], true)) {
+ ++$depth;
+ } elseif (\in_array($character, ['>', '}', ')'], true)) {
+ $depth = max(0, $depth - 1);
+ } elseif ('|' === $character && 0 === $depth) {
+ $branches[] = trim($current);
+ $current = '';
+
+ continue;
+ }
+
+ $current .= $character;
+ }
+
+ $branches[] = trim($current);
+
+ return array_values(array_filter($branches, static fn (string $branch): bool => '' !== $branch));
+ }
+
+ /**
+ * Maps a PHP type string (potentially a union) to an array of JSON Schema type names.
+ *
+ * @return string[]
+ */
+ private function mapPhpTypeToJsonSchemaType(string $phpTypeString): array
+ {
+ $normalizedType = strtolower(trim($phpTypeString));
+
+ // PRIORITY 1: Handle unions before anything else. A `|` at the top
+ // level joins alternatives; one inside `<>` or `{}` belongs to a
+ // generic (`array`) and is not a union of the whole type.
+ // Checked first because a branch may itself be an array shape — and
+ // `string[]|int` read as "an array" silently loses the `int`.
+ $branches = self::splitUnion($normalizedType);
+
+ if (\count($branches) > 1) {
+ $jsonTypes = [];
+ foreach ($branches as $branch) {
+ $jsonTypes = array_merge($jsonTypes, $this->mapPhpTypeToJsonSchemaType($branch));
+ }
+
+ return array_values(array_unique($jsonTypes));
+ }
+
+ // PRIORITY 2: Check for array{} syntax which should be treated as object
+ if (preg_match('/^array\s*{/i', $normalizedType)) {
+ return ['object'];
+ }
+
+ // PRIORITY 3: Check for array syntax (T[] or generics)
+ if (
+ str_contains($normalizedType, '[]')
+ || preg_match('/^(array|list|iterable|collection)]+\s*>$/i', $normalizedType)) {
+ return ['integer'];
+ }
+
+ // PRIORITY 5: Handle simple built-in types
+ return match ($normalizedType) {
+ 'string', 'scalar' => ['string'],
+ '?string' => ['null', 'string'],
+ 'int', 'integer' => ['integer'],
+ '?int', '?integer' => ['null', 'integer'],
+ 'float', 'double', 'number' => ['number'],
+ '?float', '?double', '?number' => ['null', 'number'],
+ 'bool', 'boolean' => ['boolean'],
+ '?bool', '?boolean' => ['null', 'boolean'],
+ 'array' => ['array'],
+ '?array' => ['null', 'array'],
+ 'object', 'stdclass' => ['object'],
+ '?object', '?stdclass' => ['null', 'object'],
+ 'null' => ['null'],
+ 'resource', 'callable' => ['object'],
+ 'mixed' => [],
+ 'void', 'never' => [],
+ default => ['object'],
+ };
+ }
+
+ /**
+ * Infers the 'items' schema type for an array based on DocBlock type hints.
+ *
+ * @return string|array
+ */
+ private function inferArrayItemsType(string $phpTypeString): string|array
+ {
+ $normalizedType = trim($phpTypeString);
+
+ // Strip a top-level nullable union (e.g. `string[]|null`, `null|int[]`) so the
+ // element type is still recovered; array nullability is handled separately via
+ // `allows_null`. Internal unions such as `array` are left untouched.
+ $normalizedType = trim((string) preg_replace('/^null\s*\|\s*|\s*\|\s*null$/i', '', $normalizedType));
+
+ // Case 1: Simple T[] syntax (e.g., string[], int[], bool[], etc.)
+ if (preg_match('/^(\\??)([\w\\\\]+)\\s*\\[\\]$/i', $normalizedType, $matches)) {
+ $itemType = strtolower($matches[2]);
+
+ return $this->mapSimpleTypeToJsonSchema($itemType);
+ }
+
+ // Case 2: Generic array syntax (e.g., array, array, etc.)
+ if (preg_match('/^(\\??)array\s*<\s*([\w\\\\|]+)\s*>$/i', $normalizedType, $matches)) {
+ $itemType = strtolower($matches[2]);
+
+ return $this->mapSimpleTypeToJsonSchema($itemType);
+ }
+
+ // Case 3: Nested array> syntax or T[][] syntax
+ if (
+ preg_match('/^(\\??)array\s*<\s*array\s*<\s*([\w\\\\|]+)\s*>\s*>$/i', $normalizedType, $matches)
+ || preg_match('/^(\\??)([\w\\\\]+)\s*\[\]\[\]$/i', $normalizedType, $matches)
+ ) {
+ $innerType = $this->mapSimpleTypeToJsonSchema(isset($matches[2]) ? strtolower($matches[2]) : 'any'); /* @phpstan-ignore isset.offset */
+
+ // Return a schema for array with items being arrays
+ return [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => $innerType,
+ ],
+ ];
+ }
+
+ // Case 4: Object-like array syntax (e.g., array{name: string, age: int})
+ if (preg_match('/^(\\??)array\s*\{(.+)\}$/is', $normalizedType, $matches)) {
+ return $this->parseObjectLikeArray($matches[2]);
+ }
+
+ return 'any';
+ }
+
+ /**
+ * Parses object-like array syntax into a JSON Schema object.
+ *
+ * @return array{
+ * type: 'object',
+ * properties?: array,
+ * required?: array
+ * }
+ */
+ private function parseObjectLikeArray(string $propertiesStr): array
+ {
+ $properties = [];
+ $required = [];
+
+ // Parse properties from the string, handling nested structures
+ $depth = 0;
+ $buffer = '';
+
+ for ($i = 0; $i < \strlen($propertiesStr); ++$i) {
+ $char = $propertiesStr[$i];
+
+ // Track nested braces
+ if ('{' === $char) {
+ ++$depth;
+ $buffer .= $char;
+ } elseif ('}' === $char) {
+ --$depth;
+ $buffer .= $char;
+ }
+ // Property separator (comma)
+ elseif (',' === $char && 0 === $depth) {
+ // Process the completed property
+ $this->parsePropertyDefinition(trim($buffer), $properties, $required);
+ $buffer = '';
+ } else {
+ $buffer .= $char;
+ }
+ }
+
+ // Process the last property
+ if (!empty($buffer)) {
+ $this->parsePropertyDefinition(trim($buffer), $properties, $required);
+ }
+
+ if (!empty($properties)) {
+ return [
+ 'type' => 'object',
+ 'properties' => $properties,
+ 'required' => $required,
+ ];
+ }
+
+ return ['type' => 'object'];
+ }
+
+ /**
+ * Parses a single property definition from an object-like array syntax.
+ */
+ private function parsePropertyDefinition(string $propDefinition, array &$properties, array &$required): void
+ {
+ // Match property name and type
+ if (preg_match('/^([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*:\s*(.+)$/i', $propDefinition, $matches)) {
+ $propName = $matches[1];
+ $propType = trim($matches[2]);
+
+ // Add to required properties
+ $required[] = $propName;
+
+ // Check for nested array{} syntax
+ if (preg_match('/^array\s*\{(.+)\}$/is', $propType, $nestedMatches)) {
+ $nestedSchema = $this->parseObjectLikeArray($nestedMatches[1]);
+ $properties[$propName] = $nestedSchema;
+ }
+ // Check for array or T[] syntax
+ elseif (
+ preg_match('/^array\s*<\s*([\w\\\\|]+)\s*>$/i', $propType, $arrayMatches)
+ || preg_match('/^([\w\\\\]+)\s*\[\]$/i', $propType, $arrayMatches)
+ ) {
+ $itemType = $arrayMatches[1] ?? 'any'; /* @phpstan-ignore nullCoalesce.offset */
+ $properties[$propName] = [
+ 'type' => 'array',
+ 'items' => [
+ 'type' => $this->mapSimpleTypeToJsonSchema($itemType),
+ ],
+ ];
+ }
+ // Simple type
+ else {
+ $properties[$propName] = ['type' => $this->mapSimpleTypeToJsonSchema($propType)];
+ }
+ }
+ }
+
+ /**
+ * Helper method to map basic PHP types to JSON Schema types.
+ */
+ private function mapSimpleTypeToJsonSchema(string $type): string
+ {
+ return match (strtolower($type)) {
+ 'string' => 'string',
+ 'int', 'integer' => 'integer',
+ 'bool', 'boolean' => 'boolean',
+ 'float', 'double', 'number' => 'number',
+ 'array' => 'array',
+ 'object', 'stdclass' => 'object',
+ default => \in_array(strtolower($type), ['datetime', 'datetimeinterface']) ? 'string' : 'object',
+ };
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/SchemaGeneratorInterface.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/SchemaGeneratorInterface.php
new file mode 100644
index 0000000..c21d3cd
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/SchemaGeneratorInterface.php
@@ -0,0 +1,41 @@
+
+ */
+interface SchemaGeneratorInterface
+{
+ /**
+ * Generates a JSON Schema for input parameters.
+ *
+ * The returned schema must be a valid JSON Schema object (type: 'object')
+ * with properties corresponding to a tool's parameters.
+ *
+ * @return array{
+ * type: 'object',
+ * properties: array|object,
+ * required?: string[]
+ * }
+ */
+ public function generate(\Reflector $reflection): array;
+
+ /**
+ * Generates a JSON Schema for output/result.
+ *
+ * @return ?array
+ */
+ public function generateOutputSchema(\Reflector $reflection): ?array;
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Discovery/SchemaValidator.php b/admin/vendor/mcp/sdk/src/Capability/Discovery/SchemaValidator.php
new file mode 100644
index 0000000..56174bd
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Discovery/SchemaValidator.php
@@ -0,0 +1,335 @@
+
+ */
+class SchemaValidator
+{
+ private ?Validator $jsonSchemaValidator = null;
+
+ public function __construct(
+ private LoggerInterface $logger = new NullLogger(),
+ ) {
+ }
+
+ /**
+ * Validates data against a JSON schema.
+ *
+ * @param mixed $data the data to validate (should generally be decoded JSON)
+ * @param array|object $schema the JSON Schema definition (as PHP array or object)
+ *
+ * @return list array of validation errors, empty if valid
+ */
+ public function validateAgainstJsonSchema(mixed $data, array|object $schema): array
+ {
+ if (\is_array($data) && empty($data)) {
+ $data = new \stdClass();
+ }
+
+ try {
+ // --- Schema Preparation ---
+ if (\is_array($schema)) {
+ $schemaJson = json_encode($schema, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES);
+ $schemaObject = json_decode($schemaJson, false, 512, \JSON_THROW_ON_ERROR);
+ } elseif (\is_object($schema)) {
+ // This might be overly cautious but safer against varied inputs.
+ $schemaJson = json_encode($schema, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES);
+ $schemaObject = json_decode($schemaJson, false, 512, \JSON_THROW_ON_ERROR);
+ } else {
+ throw new InvalidArgumentException('Schema must be an array or object.');
+ }
+
+ // --- Data Preparation ---
+ // Opis Validator generally prefers objects for object validation
+ $dataToValidate = $this->convertDataForValidator($data);
+ } catch (\JsonException $e) {
+ $this->logger->error('MCP SDK: Invalid schema structure provided for validation (JSON conversion failed).', ['exception' => $e]);
+
+ return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Invalid schema definition provided (JSON error).']];
+ } catch (InvalidArgumentException $e) {
+ $this->logger->error('MCP SDK: Invalid schema structure provided for validation.', ['exception' => $e]);
+
+ return [['pointer' => '', 'keyword' => 'internal', 'message' => $e->getMessage()]];
+ } catch (\Throwable $e) {
+ $this->logger->error('MCP SDK: Error preparing data/schema for validation.', ['exception' => $e]);
+
+ return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Internal validation preparation error.']];
+ }
+
+ $validator = $this->getJsonSchemaValidator();
+
+ try {
+ $result = $validator->validate($dataToValidate, $schemaObject);
+ } catch (\Throwable $e) {
+ $this->logger->error('MCP SDK: JSON Schema validation failed internally.', [
+ 'exception' => $e,
+ 'data' => json_encode($dataToValidate),
+ 'schema' => json_encode($schemaObject),
+ ]);
+
+ return [['pointer' => '', 'keyword' => 'internal', 'message' => 'Schema validation process failed: '.$e->getMessage()]];
+ }
+
+ if ($result->isValid()) {
+ return [];
+ }
+
+ $formattedErrors = [];
+ $topError = $result->error();
+
+ if ($topError) {
+ $this->collectSubErrors($topError, $formattedErrors);
+ }
+
+ if (empty($formattedErrors) && $topError) { // Fallback
+ $formattedErrors[] = [
+ 'pointer' => $this->formatJsonPointerPath($topError->data()->path()),
+ 'keyword' => $topError->keyword(),
+ 'message' => $this->formatValidationError($topError),
+ ];
+ }
+
+ return $formattedErrors;
+ }
+
+ /**
+ * Get or create the JSON Schema validator instance.
+ */
+ private function getJsonSchemaValidator(): Validator
+ {
+ if (null === $this->jsonSchemaValidator) {
+ $this->jsonSchemaValidator = new Validator();
+ // Potentially configure resolver here if needed later
+ }
+
+ return $this->jsonSchemaValidator;
+ }
+
+ /**
+ * Recursively converts associative arrays to stdClass objects for validator compatibility.
+ */
+ private function convertDataForValidator(mixed $data): mixed
+ {
+ if (\is_array($data)) {
+ // Check if it's an associative array (keys are not sequential numbers 0..N-1)
+ if (!empty($data) && array_keys($data) !== range(0, \count($data) - 1)) {
+ $obj = new \stdClass();
+ foreach ($data as $key => $value) {
+ $obj->{$key} = $this->convertDataForValidator($value);
+ }
+
+ return $obj;
+ }
+
+ // It's a list (sequential array), convert items recursively
+ return array_map([$this, 'convertDataForValidator'], $data);
+ } elseif (\is_object($data) && $data instanceof \stdClass) {
+ // Deep copy/convert stdClass objects as well
+ $obj = new \stdClass();
+ foreach (get_object_vars($data) as $key => $value) {
+ $obj->{$key} = $this->convertDataForValidator($value);
+ }
+
+ return $obj;
+ }
+
+ // Leave other objects and scalar types as they are
+ return $data;
+ }
+
+ /**
+ * Recursively collects leaf validation errors.
+ *
+ * @param Error[] $collectedErrors
+ */
+ private function collectSubErrors(ValidationError $error, array &$collectedErrors): void
+ {
+ $subErrors = $error->subErrors();
+ if (empty($subErrors)) {
+ $collectedErrors[] = [
+ 'pointer' => $this->formatJsonPointerPath($error->data()->path()),
+ 'keyword' => $error->keyword(),
+ 'message' => $this->formatValidationError($error),
+ ];
+ } else {
+ foreach ($subErrors as $subError) {
+ $this->collectSubErrors($subError, $collectedErrors);
+ }
+ }
+ }
+
+ /**
+ * Formats the path array into a JSON Pointer string.
+ *
+ * @param string[]|int[]|null $pathComponents
+ */
+ private function formatJsonPointerPath(?array $pathComponents): string
+ {
+ if (empty($pathComponents)) {
+ return '/';
+ }
+ $escapedComponents = array_map(static function ($component) {
+ $componentStr = (string) $component;
+
+ return str_replace(['~', '/'], ['~0', '~1'], $componentStr);
+ }, $pathComponents);
+
+ return '/'.implode('/', $escapedComponents);
+ }
+
+ /**
+ * Formats an Opis SchemaValidationError into a user-friendly message.
+ */
+ private function formatValidationError(ValidationError $error): string
+ {
+ $keyword = $error->keyword();
+ $args = $error->args();
+ $message = "Constraint `{$keyword}` failed.";
+
+ switch (strtolower($keyword)) {
+ case 'required':
+ $missing = $args['missing'] ?? [];
+ $formattedMissing = implode(', ', array_map(static fn ($p) => "`{$p}`", $missing));
+ $message = "Missing required properties: {$formattedMissing}.";
+ break;
+ case 'type':
+ $expected = implode('|', (array) ($args['expected'] ?? []));
+ $used = $error->data()->type() ?? 'unknown';
+ $message = "Invalid type. Expected `{$expected}`, but received `{$used}`.";
+ break;
+ case 'enum':
+ $schemaData = $error->schema()->info()->data();
+ $allowedValues = [];
+ if (\is_object($schemaData) && property_exists($schemaData, 'enum') && \is_array($schemaData->enum)) {
+ $allowedValues = $schemaData->enum;
+ } elseif (\is_array($schemaData) && isset($schemaData['enum']) && \is_array($schemaData['enum'])) {
+ $allowedValues = $schemaData['enum'];
+ } else {
+ $this->logger->warning("MCP SDK: Could not retrieve 'enum' values from schema info for error.", ['error_args' => $args]);
+ }
+ if (empty($allowedValues)) {
+ $message = 'Value does not match the allowed enumeration.';
+ } else {
+ $formattedAllowed = array_map(static function ($v) { /* ... formatting logic ... */
+ if (\is_string($v)) {
+ return '"'.$v.'"';
+ }
+ if (\is_bool($v)) {
+ return $v ? 'true' : 'false';
+ }
+ if (null === $v) {
+ return 'null';
+ }
+
+ return (string) $v;
+ }, $allowedValues);
+ $message = 'Value must be one of the allowed values: '.implode(', ', $formattedAllowed).'.';
+ }
+ break;
+ case 'const':
+ $expected = json_encode($args['expected'] ?? 'null', \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE);
+ $message = "Value must be equal to the constant value: {$expected}.";
+ break;
+ case 'minLength': // Corrected casing
+ $min = $args['min'] ?? '?';
+ $message = "String must be at least {$min} characters long.";
+ break;
+ case 'maxLength': // Corrected casing
+ $max = $args['max'] ?? '?';
+ $message = "String must not be longer than {$max} characters.";
+ break;
+ case 'pattern':
+ $pattern = $args['pattern'] ?? '?';
+ $message = "String does not match the required pattern: `{$pattern}`.";
+ break;
+ case 'minimum':
+ $min = $args['min'] ?? '?';
+ $message = "Number must be greater than or equal to {$min}.";
+ break;
+ case 'maximum':
+ $max = $args['max'] ?? '?';
+ $message = "Number must be less than or equal to {$max}.";
+ break;
+ case 'exclusiveMinimum': // Corrected casing
+ $min = $args['min'] ?? '?';
+ $message = "Number must be strictly greater than {$min}.";
+ break;
+ case 'exclusiveMaximum': // Corrected casing
+ $max = $args['max'] ?? '?';
+ $message = "Number must be strictly less than {$max}.";
+ break;
+ case 'multipleOf': // Corrected casing
+ $value = $args['value'] ?? '?';
+ $message = "Number must be a multiple of {$value}.";
+ break;
+ case 'minItems': // Corrected casing
+ $min = $args['min'] ?? '?';
+ $message = "Array must contain at least {$min} items.";
+ break;
+ case 'maxItems': // Corrected casing
+ $max = $args['max'] ?? '?';
+ $message = "Array must contain no more than {$max} items.";
+ break;
+ case 'uniqueItems': // Corrected casing
+ $message = 'Array items must be unique.';
+ break;
+ case 'minProperties': // Corrected casing
+ $min = $args['min'] ?? '?';
+ $message = "Object must have at least {$min} properties.";
+ break;
+ case 'maxProperties': // Corrected casing
+ $max = $args['max'] ?? '?';
+ $message = "Object must have no more than {$max} properties.";
+ break;
+ case 'additionalProperties': // Corrected casing
+ $unexpected = $args['properties'] ?? [];
+ $formattedUnexpected = implode(', ', array_map(static fn ($p) => "`{$p}`", $unexpected));
+ $message = "Object contains unexpected additional properties: {$formattedUnexpected}.";
+ break;
+ case 'format':
+ $format = $args['format'] ?? 'unknown';
+ $message = "Value does not match the required format: `{$format}`.";
+ break;
+ default:
+ $builtInMessage = $error->message();
+ if ($builtInMessage && 'The data must match the schema' !== $builtInMessage) {
+ $placeholders = $args;
+ $builtInMessage = preg_replace_callback('/\{(\w+)\}/', static function ($match) use ($placeholders) {
+ $key = $match[1];
+ $value = $placeholders[$key] ?? '{'.$key.'}';
+
+ return \is_array($value) ? json_encode($value) : (string) $value;
+ }, $builtInMessage);
+ $message = $builtInMessage;
+ }
+ break;
+ }
+
+ return $message;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Formatter/PromptResultFormatter.php b/admin/vendor/mcp/sdk/src/Capability/Formatter/PromptResultFormatter.php
new file mode 100644
index 0000000..207c7e2
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Formatter/PromptResultFormatter.php
@@ -0,0 +1,211 @@
+
+ * @author Mateu Aguiló Bosch
+ */
+final class PromptResultFormatter
+{
+ /**
+ * Formats the raw result of a prompt generator into an array of MCP PromptMessages.
+ *
+ * @param mixed $promptGenerationResult expected: array of message structures
+ *
+ * @return PromptMessage[] array of PromptMessage objects
+ *
+ * @throws \RuntimeException if the result cannot be formatted
+ * @throws \JsonException if JSON encoding fails
+ */
+ public function format(mixed $promptGenerationResult): array
+ {
+ if ($promptGenerationResult instanceof PromptMessage) {
+ return [$promptGenerationResult];
+ }
+
+ if (!\is_array($promptGenerationResult)) {
+ throw new RuntimeException('Prompt generator method must return an array of messages.');
+ }
+
+ if (empty($promptGenerationResult)) {
+ return [];
+ }
+
+ if (\is_array($promptGenerationResult)) {
+ $allArePromptMessages = true;
+ $hasPromptMessages = false;
+
+ foreach ($promptGenerationResult as $item) {
+ if ($item instanceof PromptMessage) {
+ $hasPromptMessages = true;
+ } else {
+ $allArePromptMessages = false;
+ }
+ }
+
+ if ($allArePromptMessages && $hasPromptMessages) {
+ return $promptGenerationResult;
+ }
+
+ if ($hasPromptMessages) {
+ $result = [];
+ foreach ($promptGenerationResult as $index => $item) {
+ if ($item instanceof PromptMessage) {
+ $result[] = $item;
+ } else {
+ $result = array_merge($result, $this->format($item));
+ }
+ }
+
+ return $result;
+ }
+
+ if (!array_is_list($promptGenerationResult)) {
+ if (isset($promptGenerationResult['user']) || isset($promptGenerationResult['assistant'])) {
+ $result = [];
+ if (isset($promptGenerationResult['user'])) {
+ $userContent = $this->formatContent($promptGenerationResult['user']);
+ $result[] = new PromptMessage(Role::User, $userContent);
+ }
+ if (isset($promptGenerationResult['assistant'])) {
+ $assistantContent = $this->formatContent($promptGenerationResult['assistant']);
+ $result[] = new PromptMessage(Role::Assistant, $assistantContent);
+ }
+
+ return $result;
+ }
+
+ if (isset($promptGenerationResult['role']) && isset($promptGenerationResult['content'])) {
+ return [$this->formatMessage($promptGenerationResult)];
+ }
+
+ throw new RuntimeException('Associative array must contain either role/content keys or user/assistant keys.');
+ }
+
+ $formattedMessages = [];
+ foreach ($promptGenerationResult as $index => $message) {
+ if ($message instanceof PromptMessage) {
+ $formattedMessages[] = $message;
+ } else {
+ $formattedMessages[] = $this->formatMessage($message, $index);
+ }
+ }
+
+ return $formattedMessages;
+ }
+
+ throw new RuntimeException('Invalid prompt generation result format.');
+ }
+
+ /**
+ * Formats a single message into a PromptMessage.
+ */
+ private function formatMessage(mixed $message, ?int $index = null): PromptMessage
+ {
+ $indexStr = null !== $index ? " at index {$index}" : '';
+
+ if (!\is_array($message) || !\array_key_exists('role', $message) || !\array_key_exists('content', $message)) {
+ throw new RuntimeException("Invalid message format{$indexStr}. Expected an array with 'role' and 'content' keys.");
+ }
+
+ $role = $message['role'] instanceof Role ? $message['role'] : Role::tryFrom($message['role']);
+ if (null === $role) {
+ throw new RuntimeException("Invalid role '{$message['role']}' in prompt message{$indexStr}. Only 'user' or 'assistant' are supported.");
+ }
+
+ $content = $this->formatContent($message['content'], $index);
+
+ return new PromptMessage($role, $content);
+ }
+
+ /**
+ * Formats content into a proper Content object.
+ */
+ private function formatContent(mixed $content, ?int $index = null): TextContent|ImageContent|AudioContent|ResourceLink|EmbeddedResource
+ {
+ $indexStr = null !== $index ? " at index {$index}" : '';
+
+ if ($content instanceof Content) {
+ if (
+ $content instanceof TextContent || $content instanceof ImageContent
+ || $content instanceof AudioContent || $content instanceof ResourceLink
+ || $content instanceof EmbeddedResource
+ ) {
+ return $content;
+ }
+ throw new RuntimeException("Invalid Content type{$indexStr}. PromptMessage only supports TextContent, ImageContent, AudioContent, ResourceLink, or EmbeddedResource.");
+ }
+
+ if (\is_string($content)) {
+ return new TextContent($content);
+ }
+
+ if (\is_array($content) && isset($content['type'])) {
+ return $this->formatTypedContent($content, $index);
+ }
+
+ if (\is_scalar($content) || null === $content) {
+ $stringContent = null === $content ? '(null)' : (\is_bool($content) ? ($content ? 'true' : 'false') : (string) $content);
+
+ return new TextContent($stringContent);
+ }
+
+ $jsonContent = json_encode($content, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR);
+
+ return new TextContent($jsonContent);
+ }
+
+ /**
+ * Formats typed content arrays into Content objects.
+ *
+ * Delegates to the schema classes' fromArray() so optional fields
+ * (annotations, mimeType, _meta, ...) carry over instead of being dropped.
+ *
+ * @param array $content
+ */
+ private function formatTypedContent(array $content, ?int $index = null): TextContent|ImageContent|AudioContent|ResourceLink|EmbeddedResource
+ {
+ $indexStr = null !== $index ? " at index {$index}" : '';
+ $type = $content['type'];
+
+ if ('resource' === $type && isset($content['resource']) && \is_array($content['resource']) && !isset($content['resource']['mimeType'])) {
+ // EmbeddedResource::fromArray() leaves a missing mimeType unset; this
+ // formatter has always defaulted it, so keep that for compatibility.
+ $content['resource']['mimeType'] = isset($content['resource']['text']) ? 'text/plain' : 'application/octet-stream';
+ }
+
+ try {
+ return match ($type) {
+ 'text' => TextContent::fromArray($content),
+ 'image' => ImageContent::fromArray($content),
+ 'audio' => AudioContent::fromArray($content),
+ 'resource' => EmbeddedResource::fromArray($content),
+ 'resource_link' => ResourceLink::fromArray($content),
+ default => throw new RuntimeException("Invalid content type '{$type}'{$indexStr}."),
+ };
+ } catch (InvalidArgumentException $e) {
+ throw new RuntimeException("Invalid '{$type}' content{$indexStr}: {$e->getMessage()}", 0, $e);
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Formatter/ResourceResultFormatter.php b/admin/vendor/mcp/sdk/src/Capability/Formatter/ResourceResultFormatter.php
new file mode 100644
index 0000000..bac2bc7
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Formatter/ResourceResultFormatter.php
@@ -0,0 +1,200 @@
+
+ * @author Mateu Aguiló Bosch
+ */
+final class ResourceResultFormatter
+{
+ /**
+ * Formats the raw result of a resource read operation into MCP ResourceContent items.
+ *
+ * @param mixed $readResult the raw result from the resource handler method
+ * @param string $uri the URI of the resource that was read
+ * @param string|null $mimeType the MIME type from the ResourceDefinition
+ * @param mixed $meta optional metadata to include in the ResourceContents
+ *
+ * @return ResourceContents[] array of ResourceContents objects
+ *
+ * @throws RuntimeException If the result cannot be formatted.
+ *
+ * Supported result types:
+ * - ResourceContents: Used as-is
+ * - EmbeddedResource: Resource is extracted from the EmbeddedResource
+ * - string: Converted to text content with guessed or provided MIME type
+ * - stream resource: Read and converted to blob with provided MIME type
+ * - array with 'blob' key: Used as blob content
+ * - array with 'text' key: Used as text content
+ * - SplFileInfo: Read and converted to blob
+ * - array: Converted to JSON if MIME type is application/json or contains 'json'
+ * For other MIME types, will try to convert to JSON with a warning
+ */
+ public function format(mixed $readResult, string $uri, ?string $mimeType = null, mixed $meta = null): array
+ {
+ if ($readResult instanceof ResourceContents) {
+ return [$readResult];
+ }
+
+ if ($readResult instanceof EmbeddedResource) {
+ return [$readResult->resource];
+ }
+
+ if (\is_array($readResult)) {
+ if (empty($readResult)) {
+ return [new TextResourceContents($uri, 'application/json', '[]', $meta)];
+ }
+
+ $allAreResourceContents = true;
+ $hasResourceContents = false;
+ $allAreEmbeddedResource = true;
+ $hasEmbeddedResource = false;
+
+ foreach ($readResult as $item) {
+ if ($item instanceof ResourceContents) {
+ $hasResourceContents = true;
+ $allAreEmbeddedResource = false;
+ } elseif ($item instanceof EmbeddedResource) {
+ $hasEmbeddedResource = true;
+ $allAreResourceContents = false;
+ } else {
+ $allAreResourceContents = false;
+ $allAreEmbeddedResource = false;
+ }
+ }
+
+ if ($allAreResourceContents && $hasResourceContents) {
+ return $readResult;
+ }
+
+ if ($allAreEmbeddedResource && $hasEmbeddedResource) {
+ return array_map(static fn ($item) => $item->resource, $readResult);
+ }
+
+ if ($hasResourceContents || $hasEmbeddedResource) {
+ $result = [];
+ foreach ($readResult as $item) {
+ if ($item instanceof ResourceContents) {
+ $result[] = $item;
+ } elseif ($item instanceof EmbeddedResource) {
+ $result[] = $item->resource;
+ } else {
+ $result = array_merge($result, $this->format($item, $uri, $mimeType, $meta));
+ }
+ }
+
+ return $result;
+ }
+ }
+
+ if (\is_string($readResult)) {
+ $mimeType = $mimeType ?? $this->guessMimeTypeFromString($readResult);
+
+ return [new TextResourceContents($uri, $mimeType, $readResult, $meta)];
+ }
+
+ if (\is_resource($readResult) && 'stream' === get_resource_type($readResult)) {
+ $result = BlobResourceContents::fromStream(
+ $uri,
+ $readResult,
+ $mimeType ?? 'application/octet-stream',
+ $meta
+ );
+
+ @fclose($readResult);
+
+ return [$result];
+ }
+
+ if (\is_array($readResult) && isset($readResult['blob']) && \is_string($readResult['blob'])) {
+ $mimeType = $readResult['mimeType'] ?? $mimeType ?? 'application/octet-stream';
+
+ return [new BlobResourceContents($uri, $mimeType, $readResult['blob'], $meta)];
+ }
+
+ if (\is_array($readResult) && isset($readResult['text']) && \is_string($readResult['text'])) {
+ $mimeType = $readResult['mimeType'] ?? $mimeType ?? 'text/plain';
+
+ return [new TextResourceContents($uri, $mimeType, $readResult['text'], $meta)];
+ }
+
+ if ($readResult instanceof \SplFileInfo && $readResult->isFile() && $readResult->isReadable()) {
+ if ($mimeType && str_contains(strtolower($mimeType), 'text')) {
+ return [new TextResourceContents($uri, $mimeType, file_get_contents($readResult->getPathname()), $meta)];
+ }
+
+ return [BlobResourceContents::fromSplFileInfo($uri, $readResult, $mimeType, $meta)];
+ }
+
+ if (\is_array($readResult)) {
+ if ($mimeType && (str_contains(strtolower($mimeType), 'json')
+ || 'application/json' === $mimeType)) {
+ try {
+ $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT);
+
+ return [new TextResourceContents($uri, $mimeType, $jsonString, $meta)];
+ } catch (\JsonException $e) {
+ throw new RuntimeException(\sprintf('Failed to encode array as JSON for URI "%s": %s', $uri, $e->getMessage()));
+ }
+ }
+
+ try {
+ $jsonString = json_encode($readResult, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT);
+ $mimeType = $mimeType ?? 'application/json';
+
+ return [new TextResourceContents($uri, $mimeType, $jsonString, $meta)];
+ } catch (\JsonException $e) {
+ throw new RuntimeException(\sprintf('Failed to encode array as JSON for URI "%s": %s', $uri, $e->getMessage()));
+ }
+ }
+
+ throw new RuntimeException(\sprintf('Cannot format resource read result for URI "%s". Handler method returned unhandled type: ', $uri).\gettype($readResult));
+ }
+
+ /**
+ * Guesses MIME type from string content (very basic).
+ */
+ private function guessMimeTypeFromString(string $content): string
+ {
+ $trimmed = ltrim($content);
+
+ if (str_starts_with($trimmed, '<') && str_ends_with(rtrim($content), '>')) {
+ if (str_contains($trimmed, '
+ * @author Mateu Aguiló Bosch
+ */
+final class ToolResultFormatter
+{
+ /**
+ * Formats the result of a tool execution into an array of MCP Content items.
+ *
+ * - If the result is already a Content object, it's wrapped in an array.
+ * - If the result is an array:
+ * - If all elements are Content objects, the array is returned as is.
+ * - If it's a mixed array (Content and non-Content items), non-Content items are
+ * individually formatted (scalars to TextContent, others to JSON TextContent).
+ * - If it's an array with no Content items, the entire array is JSON-encoded into a single TextContent.
+ * - Scalars (string, int, float, bool) are wrapped in TextContent.
+ * - null is represented as TextContent('(null)').
+ * - Other objects are JSON-encoded and wrapped in TextContent.
+ *
+ * @param mixed $toolExecutionResult the raw value returned by the tool's PHP method
+ *
+ * @return Content[] the content items for CallToolResult
+ *
+ * @throws \JsonException if JSON encoding fails for non-Content array/object results
+ */
+ public function format(mixed $toolExecutionResult): array
+ {
+ if ($toolExecutionResult instanceof Content) {
+ return [$toolExecutionResult];
+ }
+
+ if (\is_array($toolExecutionResult)) {
+ if (empty($toolExecutionResult)) {
+ return [new TextContent('[]')];
+ }
+
+ $allAreContent = true;
+ $hasContent = false;
+
+ foreach ($toolExecutionResult as $item) {
+ if ($item instanceof Content) {
+ $hasContent = true;
+ } else {
+ $allAreContent = false;
+ }
+ }
+
+ if ($allAreContent && $hasContent) {
+ return $toolExecutionResult;
+ }
+
+ if ($hasContent) {
+ $result = [];
+ foreach ($toolExecutionResult as $item) {
+ if ($item instanceof Content) {
+ $result[] = $item;
+ } else {
+ $result = array_merge($result, $this->format($item));
+ }
+ }
+
+ return $result;
+ }
+ }
+
+ if (null === $toolExecutionResult) {
+ return [new TextContent('(null)')];
+ }
+
+ if (\is_bool($toolExecutionResult)) {
+ return [new TextContent($toolExecutionResult ? 'true' : 'false')];
+ }
+
+ if (\is_scalar($toolExecutionResult)) {
+ return [new TextContent($toolExecutionResult)];
+ }
+
+ $jsonResult = json_encode(
+ $toolExecutionResult,
+ \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR | \JSON_INVALID_UTF8_SUBSTITUTE
+ );
+
+ return [new TextContent($jsonResult)];
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Logger/ClientLogger.php b/admin/vendor/mcp/sdk/src/Capability/Logger/ClientLogger.php
new file mode 100644
index 0000000..f94deb6
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Logger/ClientLogger.php
@@ -0,0 +1,82 @@
+
+ * @author Tobias Nyholm
+ *
+ * @deprecated since protocol revision 2026-07-28 (SEP-2577), earliest removal 2027-07-28.
+ * Log to stderr (stdio) or use OpenTelemetry instead.
+ */
+final class ClientLogger extends AbstractLogger
+{
+ public function __construct(
+ private ClientGateway $client,
+ private SessionInterface $session,
+ ) {
+ }
+
+ /**
+ * Logs with an arbitrary level.
+ *
+ * @param string|\Stringable $message
+ * @param array $context
+ */
+ public function log($level, $message, array $context = []): void
+ {
+ // Convert PSR-3 level to MCP LoggingLevel
+ $mcpLevel = $this->convertToMcpLevel($level);
+ if (null === $mcpLevel) {
+ return; // Unknown level, skip MCP notification
+ }
+
+ $minimumLevel = $this->session->get(Protocol::SESSION_LOGGING_LEVEL, '');
+ $minimumLevel = LoggingLevel::tryFrom($minimumLevel) ?? LoggingLevel::Warning;
+
+ if (!$mcpLevel->isAtLeast($minimumLevel)) {
+ return;
+ }
+
+ $this->client->log($mcpLevel, $message);
+ }
+
+ /**
+ * Converts PSR-3 log level to MCP LoggingLevel.
+ *
+ * @param mixed $level PSR-3 level
+ *
+ * @return LoggingLevel|null MCP level or null if unknown
+ */
+ private function convertToMcpLevel($level): ?LoggingLevel
+ {
+ return match (strtolower((string) $level)) {
+ 'emergency' => LoggingLevel::Emergency,
+ 'alert' => LoggingLevel::Alert,
+ 'critical' => LoggingLevel::Critical,
+ 'error' => LoggingLevel::Error,
+ 'warning' => LoggingLevel::Warning,
+ 'notice' => LoggingLevel::Notice,
+ 'info' => LoggingLevel::Info,
+ 'debug' => LoggingLevel::Debug,
+ default => null,
+ };
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry.php b/admin/vendor/mcp/sdk/src/Capability/Registry.php
new file mode 100644
index 0000000..9784043
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry.php
@@ -0,0 +1,480 @@
+
+ */
+final class Registry implements RegistryInterface
+{
+ /**
+ * @var array
+ */
+ private array $tools = [];
+
+ /**
+ * @var array
+ */
+ private array $resources = [];
+
+ /**
+ * @var array
+ */
+ private array $prompts = [];
+
+ /**
+ * @var array
+ */
+ private array $resourceTemplates = [];
+
+ private bool $loaded = false;
+
+ private bool $loading = false;
+
+ public function __construct(
+ private readonly ?EventDispatcherInterface $eventDispatcher = null,
+ private readonly LoggerInterface $logger = new NullLogger(),
+ private readonly NameValidator $nameValidator = new NameValidator(),
+ private readonly ?LoaderInterface $loader = null,
+ ) {
+ }
+
+ /**
+ * Runs the configured loader once, on demand. Reads trigger this automatically, so element
+ * loading is deferred to the first read (request time) rather than eager at build time — under a
+ * persistent runtime a source not yet ready at build no longer freezes the registry empty.
+ *
+ * `loaded` is set only after success, so a transient failure is retried on the next read. The
+ * `loading` guard lets a loader read the registry during its own run (e.g. discovery's identity
+ * check) without re-entering the load.
+ */
+ public function load(): void
+ {
+ if ($this->loaded || $this->loading || null === $this->loader) {
+ return;
+ }
+
+ $this->loadFrom($this->loader);
+
+ $this->loaded = true;
+ }
+
+ /**
+ * Runs $loader with the change events its registrations would dispatch suppressed, since they
+ * describe the registry filling up rather than changing.
+ *
+ * Re-entrant-safe, and failure propagates. Does not mark the registry loaded: $loader is the
+ * caller's, and the one the constructor took is still owed its run.
+ */
+ public function loadFrom(LoaderInterface $loader): void
+ {
+ if ($this->loading) {
+ return;
+ }
+
+ $this->loading = true;
+ try {
+ $loader->load($this);
+ } finally {
+ $this->loading = false;
+ }
+ }
+
+ public function registerTool(Tool $tool, callable|array|string $handler): ToolReference
+ {
+ if (!$this->nameValidator->isValid($tool->name)) {
+ $this->logger->warning(
+ \sprintf('Tool name "%s" is invalid. Tool names should only contain letters (a-z, A-Z), numbers, dots, hyphens, underscores, and forward slashes.', $tool->name),
+ );
+ }
+
+ $reference = new ToolReference($tool, $handler);
+ $this->tools[$tool->name] = $reference;
+
+ $this->dispatch(new ToolListChangedEvent());
+
+ return $reference;
+ }
+
+ public function registerResource(ResourceDefinition $resource, callable|array|string $handler): ResourceReference
+ {
+ $reference = new ResourceReference($resource, $handler);
+ $this->resources[$resource->uri] = $reference;
+
+ $this->dispatch(new ResourceListChangedEvent());
+
+ return $reference;
+ }
+
+ public function registerResourceTemplate(
+ ResourceTemplate $template,
+ callable|array|string $handler,
+ array $completionProviders = [],
+ ): ResourceTemplateReference {
+ $reference = new ResourceTemplateReference($template, $handler, $completionProviders);
+ $this->resourceTemplates[$template->uriTemplate] = $reference;
+
+ $this->dispatch(new ResourceTemplateListChangedEvent());
+
+ return $reference;
+ }
+
+ public function registerPrompt(
+ Prompt $prompt,
+ callable|array|string $handler,
+ array $completionProviders = [],
+ ): PromptReference {
+ $reference = new PromptReference($prompt, $handler, $completionProviders);
+ $this->prompts[$prompt->name] = $reference;
+
+ $this->dispatch(new PromptListChangedEvent());
+
+ return $reference;
+ }
+
+ public function unregisterTool(string $name): void
+ {
+ if (!isset($this->tools[$name])) {
+ return;
+ }
+
+ unset($this->tools[$name]);
+
+ $this->dispatch(new ToolListChangedEvent());
+ }
+
+ public function unregisterResource(string $uri): void
+ {
+ if (!isset($this->resources[$uri])) {
+ return;
+ }
+
+ unset($this->resources[$uri]);
+
+ $this->dispatch(new ResourceListChangedEvent());
+ }
+
+ public function unregisterResourceTemplate(string $uriTemplate): void
+ {
+ if (!isset($this->resourceTemplates[$uriTemplate])) {
+ return;
+ }
+
+ unset($this->resourceTemplates[$uriTemplate]);
+
+ $this->dispatch(new ResourceTemplateListChangedEvent());
+ }
+
+ public function unregisterPrompt(string $name): void
+ {
+ if (!isset($this->prompts[$name])) {
+ return;
+ }
+
+ unset($this->prompts[$name]);
+
+ $this->dispatch(new PromptListChangedEvent());
+ }
+
+ public function hasTool(string $name): bool
+ {
+ $this->load();
+
+ return isset($this->tools[$name]);
+ }
+
+ public function hasResource(string $uri): bool
+ {
+ $this->load();
+
+ return isset($this->resources[$uri]);
+ }
+
+ public function hasResourceTemplate(string $uriTemplate): bool
+ {
+ $this->load();
+
+ return isset($this->resourceTemplates[$uriTemplate]);
+ }
+
+ public function hasPrompt(string $name): bool
+ {
+ $this->load();
+
+ return isset($this->prompts[$name]);
+ }
+
+ public function hasTools(): bool
+ {
+ $this->load();
+
+ return [] !== $this->tools;
+ }
+
+ public function getTools(?int $limit = null, ?string $cursor = null): Page
+ {
+ $this->load();
+
+ $tools = [];
+ foreach ($this->tools as $toolReference) {
+ $tools[$toolReference->tool->name] = $toolReference->tool;
+ }
+
+ if (null === $limit) {
+ return new Page($tools, null);
+ }
+
+ $paginatedTools = $this->paginateResults($tools, $limit, $cursor);
+
+ $nextCursor = $this->calculateNextCursor(
+ \count($tools),
+ $cursor,
+ $limit
+ );
+
+ return new Page($paginatedTools, $nextCursor);
+ }
+
+ public function getTool(string $name): ToolReference
+ {
+ $this->load();
+
+ return $this->tools[$name] ?? throw new ToolNotFoundException($name);
+ }
+
+ public function hasResources(): bool
+ {
+ $this->load();
+
+ return [] !== $this->resources;
+ }
+
+ public function getResources(?int $limit = null, ?string $cursor = null): Page
+ {
+ $this->load();
+
+ $resources = [];
+ foreach ($this->resources as $resourceReference) {
+ $resources[$resourceReference->resource->uri] = $resourceReference->resource;
+ }
+
+ if (null === $limit) {
+ return new Page($resources, null);
+ }
+
+ $paginatedResources = $this->paginateResults($resources, $limit, $cursor);
+
+ $nextCursor = $this->calculateNextCursor(
+ \count($resources),
+ $cursor,
+ $limit
+ );
+
+ return new Page($paginatedResources, $nextCursor);
+ }
+
+ public function getResource(
+ string $uri,
+ bool $includeTemplates = true,
+ ): ResourceReference|ResourceTemplateReference {
+ $this->load();
+
+ $registration = $this->resources[$uri] ?? null;
+ if ($registration) {
+ return $registration;
+ }
+
+ if ($includeTemplates) {
+ foreach ($this->resourceTemplates as $template) {
+ if ($template->matches($uri)) {
+ return $template;
+ }
+ }
+ }
+
+ $this->logger->debug('No resource matched URI.', ['uri' => $uri]);
+
+ throw new ResourceNotFoundException($uri);
+ }
+
+ public function hasResourceTemplates(): bool
+ {
+ $this->load();
+
+ return [] !== $this->resourceTemplates;
+ }
+
+ public function getResourceTemplates(?int $limit = null, ?string $cursor = null): Page
+ {
+ $this->load();
+
+ $templates = [];
+ foreach ($this->resourceTemplates as $templateReference) {
+ $templates[$templateReference->resourceTemplate->uriTemplate] = $templateReference->resourceTemplate;
+ }
+
+ if (null === $limit) {
+ return new Page($templates, null);
+ }
+
+ $paginatedTemplates = $this->paginateResults($templates, $limit, $cursor);
+
+ $nextCursor = $this->calculateNextCursor(
+ \count($templates),
+ $cursor,
+ $limit
+ );
+
+ return new Page($paginatedTemplates, $nextCursor);
+ }
+
+ public function getResourceTemplate(string $uriTemplate): ResourceTemplateReference
+ {
+ $this->load();
+
+ return $this->resourceTemplates[$uriTemplate] ?? throw new ResourceNotFoundException($uriTemplate);
+ }
+
+ public function hasPrompts(): bool
+ {
+ $this->load();
+
+ return [] !== $this->prompts;
+ }
+
+ public function getPrompts(?int $limit = null, ?string $cursor = null): Page
+ {
+ $this->load();
+
+ $prompts = [];
+ foreach ($this->prompts as $promptReference) {
+ $prompts[$promptReference->prompt->name] = $promptReference->prompt;
+ }
+
+ if (null === $limit) {
+ return new Page($prompts, null);
+ }
+
+ $paginatedPrompts = $this->paginateResults($prompts, $limit, $cursor);
+
+ $nextCursor = $this->calculateNextCursor(
+ \count($prompts),
+ $cursor,
+ $limit
+ );
+
+ return new Page($paginatedPrompts, $nextCursor);
+ }
+
+ public function getPrompt(string $name): PromptReference
+ {
+ $this->load();
+
+ return $this->prompts[$name] ?? throw new PromptNotFoundException($name);
+ }
+
+ /**
+ * Suppressed while the deferred loader runs, since it only sets up initial state.
+ */
+ private function dispatch(object $event): void
+ {
+ if ($this->loading) {
+ return;
+ }
+
+ $this->eventDispatcher?->dispatch($event);
+ }
+
+ /**
+ * Calculate next cursor for pagination.
+ *
+ * @param int $totalItems Count of all items
+ * @param string|null $currentCursor Current cursor position
+ * @param int $limit Number requested/returned per page
+ */
+ private function calculateNextCursor(int $totalItems, ?string $currentCursor, int $limit): ?string
+ {
+ $currentOffset = 0;
+
+ if (null !== $currentCursor) {
+ $decodedCursor = base64_decode($currentCursor, true);
+ if (false !== $decodedCursor && is_numeric($decodedCursor)) {
+ $currentOffset = (int) $decodedCursor;
+ }
+ }
+
+ $nextOffset = $currentOffset + $limit;
+
+ if ($nextOffset < $totalItems) {
+ return base64_encode((string) $nextOffset);
+ }
+
+ return null;
+ }
+
+ /**
+ * Helper method to paginate results using cursor-based pagination.
+ *
+ * @param array $items The full array of items to paginate The full array of items to paginate
+ * @param int $limit Maximum number of items to return
+ * @param string|null $cursor Base64 encoded offset position
+ *
+ * @return array Paginated results
+ *
+ * @throws InvalidCursorException When cursor is invalid (MCP error code -32602)
+ */
+ private function paginateResults(array $items, int $limit, ?string $cursor = null): array
+ {
+ $offset = 0;
+ if (null !== $cursor) {
+ $decodedCursor = base64_decode($cursor, true);
+
+ if (false === $decodedCursor || !is_numeric($decodedCursor)) {
+ throw new InvalidCursorException($cursor);
+ }
+
+ $offset = (int) $decodedCursor;
+
+ // Validate offset is within reasonable bounds
+ if ($offset < 0 || $offset > \count($items)) {
+ throw new InvalidCursorException($cursor);
+ }
+ }
+
+ return array_values(\array_slice($items, $offset, $limit));
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/Container.php b/admin/vendor/mcp/sdk/src/Capability/Registry/Container.php
new file mode 100644
index 0000000..92c4a90
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/Container.php
@@ -0,0 +1,188 @@
+
+ */
+final class Container implements ContainerInterface
+{
+ /**
+ * @var array Cache for already created instances (shared singletons)
+ */
+ private array $instances = [];
+
+ /**
+ * @var array Track classes currently being resolved to detect circular dependencies
+ */
+ private array $resolving = [];
+
+ /**
+ * Finds an entry of the container by its identifier and returns it.
+ *
+ * @param string $id identifier of the entry to look for (usually a FQCN)
+ *
+ * @return mixed entry
+ *
+ * @throws NotFoundExceptionInterface no entry was found for **this** identifier
+ * @throws ContainerExceptionInterface Error while retrieving the entry (e.g., dependency resolution failure, circular dependency).
+ */
+ public function get(string $id): mixed
+ {
+ // 1. Check instance cache
+ if (isset($this->instances[$id])) {
+ return $this->instances[$id];
+ }
+
+ // 2. Check if class exists
+ if (!class_exists($id) && !interface_exists($id)) { // Also check interface for bindings
+ throw new ServiceNotFoundException(\sprintf('Class, interface, or entry "%s" not found.', $id));
+ }
+
+ // 7. Circular Dependency Check
+ if (isset($this->resolving[$id])) {
+ throw new ContainerException("Circular dependency detected while resolving '{$id}'. Resolution path: ".implode(' -> ', array_keys($this->resolving))." -> {$id}");
+ }
+
+ $this->resolving[$id] = true; // Mark as currently resolving
+
+ try {
+ // 3. Reflect on the class
+ $reflector = new \ReflectionClass($id);
+
+ // Check if class is instantiable (abstract classes, interfaces cannot be directly instantiated)
+ if (!$reflector->isInstantiable()) {
+ // We might have an interface bound to a concrete class via set()
+ // This check is slightly redundant due to class_exists but good practice
+ throw new ContainerException("Class '{$id}' is not instantiable (e.g., abstract class or interface without explicit binding).");
+ }
+
+ // 4. Get the constructor
+ $constructor = $reflector->getConstructor();
+
+ // 5. If no constructor or constructor has no parameters, instantiate directly
+ if (null === $constructor || 0 === $constructor->getNumberOfParameters()) {
+ $instance = $reflector->newInstance();
+ } else {
+ // 6. Constructor has parameters, attempt to resolve them
+ $parameters = $constructor->getParameters();
+ $resolvedArgs = [];
+
+ foreach ($parameters as $parameter) {
+ $resolvedArgs[] = $this->resolveParameter($parameter, $id);
+ }
+
+ // Instantiate with resolved arguments
+ $instance = $reflector->newInstanceArgs($resolvedArgs);
+ }
+
+ // Cache the instance
+ $this->instances[$id] = $instance;
+
+ return $instance;
+ } catch (\ReflectionException $e) {
+ throw new ContainerException(\sprintf('Reflection failed for %s.', $id), 0, $e);
+ } catch (ContainerExceptionInterface $e) { // Re-throw container exceptions directly
+ throw $e;
+ } catch (\Throwable $e) { // Catch other instantiation errors
+ throw new ContainerException("Failed to instantiate or resolve dependencies for '{$id}': ".$e->getMessage(), (int) $e->getCode(), $e);
+ } finally {
+ // 7. Remove from resolving stack once done (success or failure)
+ unset($this->resolving[$id]);
+ }
+ }
+
+ /**
+ * Attempts to resolve a single constructor parameter.
+ *
+ * @throws ContainerExceptionInterface if a required dependency cannot be resolved
+ */
+ private function resolveParameter(\ReflectionParameter $parameter, string $consumerClassId): mixed
+ {
+ // Check for type hint
+ $type = $parameter->getType();
+
+ if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
+ // Type hint is a class or interface name
+ $typeName = $type->getName();
+ try {
+ // Recursively get the dependency
+ return $this->get($typeName);
+ } catch (NotFoundExceptionInterface $e) {
+ // Dependency class not found, fail ONLY if required
+ if (!$parameter->isOptional() && !$parameter->allowsNull()) {
+ throw new ContainerException("Unresolvable dependency '{$typeName}' required by '{$consumerClassId}' constructor parameter \${$parameter->getName()}.", 0, $e);
+ }
+ // If optional or nullable, proceed (will check allowsNull/Default below)
+ } catch (ContainerExceptionInterface $e) {
+ // Dependency itself failed to resolve (e.g., its own deps, circular)
+ throw new ContainerException("Failed to resolve dependency '{$typeName}' for '{$consumerClassId}' parameter \${$parameter->getName()}: ".$e->getMessage(), 0, $e);
+ }
+ }
+
+ // Check if parameter has a default value
+ if ($parameter->isDefaultValueAvailable()) {
+ return $parameter->getDefaultValue();
+ }
+
+ // Check if parameter allows null (and wasn't resolved above)
+ if ($parameter->allowsNull()) {
+ return null;
+ }
+
+ // Check if it was a built-in type without a default (unresolvable by this basic container)
+ if ($type instanceof \ReflectionNamedType && $type->isBuiltin()) {
+ throw new ContainerException("Cannot auto-wire built-in type '{$type->getName()}' for required parameter \${$parameter->getName()} in '{$consumerClassId}' constructor. Provide a default value or use a more advanced container.");
+ }
+
+ // Check if it was a union/intersection type without a default (also unresolvable)
+ if (null !== $type && !$type instanceof \ReflectionNamedType) {
+ throw new ContainerException("Cannot auto-wire complex type (union/intersection) for required parameter \${$parameter->getName()} in '{$consumerClassId}' constructor. Provide a default value or use a more advanced container.");
+ }
+
+ // If we reach here, it's an untyped, required parameter without a default.
+ // Or potentially an unresolvable optional class dependency where null is not allowed (edge case).
+ throw new ContainerException("Cannot resolve required parameter \${$parameter->getName()} for '{$consumerClassId}' constructor (untyped or unresolvable complex type).");
+ }
+
+ /**
+ * Returns true if the container can return an entry for the given identifier.
+ * Checks explicitly set instances and if the class/interface exists.
+ * Does not guarantee `get()` will succeed if auto-wiring fails.
+ */
+ public function has(string $id): bool
+ {
+ return isset($this->instances[$id]) || class_exists($id) || interface_exists($id);
+ }
+
+ /**
+ * Adds a pre-built instance or a factory/binding to the container.
+ * This basic version only supports pre-built instances (singletons).
+ */
+ public function set(string $id, object $instance): void
+ {
+ // Could add support for closures/factories later if needed
+ $this->instances[$id] = $instance;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/ElementReference.php b/admin/vendor/mcp/sdk/src/Capability/Registry/ElementReference.php
new file mode 100644
index 0000000..a49b2eb
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/ElementReference.php
@@ -0,0 +1,28 @@
+
+ */
+class ElementReference
+{
+ /**
+ * @param Handler $handler
+ */
+ public function __construct(
+ public readonly \Closure|array|string $handler,
+ ) {
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/InjectableParameters.php b/admin/vendor/mcp/sdk/src/Capability/Registry/InjectableParameters.php
new file mode 100644
index 0000000..4e8eba2
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/InjectableParameters.php
@@ -0,0 +1,63 @@
+ $arguments the raw argument bag including the internal "_session" and "_request" entries
+ */
+ public static function resolve(string $typeName, array $arguments): ?object
+ {
+ if (RequestContext::class === $typeName && isset($arguments['_session'], $arguments['_request'])) {
+ return new RequestContext($arguments['_session'], $arguments['_request']);
+ }
+
+ if (ClientGateway::class === $typeName && isset($arguments['_session'])) {
+ return new ClientGateway($arguments['_session']);
+ }
+
+ return null;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/ChainLoader.php b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/ChainLoader.php
new file mode 100644
index 0000000..6c30a16
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/ChainLoader.php
@@ -0,0 +1,38 @@
+
+ */
+final class ChainLoader implements LoaderInterface
+{
+ /**
+ * @param LoaderInterface[] $loaders
+ */
+ public function __construct(
+ private readonly array $loaders,
+ ) {
+ }
+
+ public function load(RegistryInterface $registry): void
+ {
+ foreach ($this->loaders as $loader) {
+ $loader->load($registry);
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/DiscoveryLoader.php b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/DiscoveryLoader.php
new file mode 100644
index 0000000..d2b9538
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/DiscoveryLoader.php
@@ -0,0 +1,185 @@
+
+ */
+final class DiscoveryLoader implements LoaderInterface
+{
+ private DiscoveryState $owned;
+
+ /**
+ * @param string[] $scanDirs
+ * @param string[] $excludeDirs
+ * @param string[] $namePatterns
+ */
+ public function __construct(
+ private string $basePath,
+ private array $scanDirs,
+ private array $excludeDirs,
+ private DiscovererInterface $discoverer,
+ private array $namePatterns = DiscovererInterface::DEFAULT_NAME_PATERNS,
+ private LoggerInterface $logger = new NullLogger(),
+ ) {
+ $this->owned = new DiscoveryState();
+ }
+
+ public function load(RegistryInterface $registry): void
+ {
+ $discovered = $this->discoverer->discover($this->basePath, $this->scanDirs, $this->excludeDirs, $this->namePatterns);
+
+ $this->unregisterOwned($registry, $this->owned->obsoletedBy($discovered));
+ $this->owned = $this->writeDiscovered($registry, $discovered);
+ }
+
+ /**
+ * Unregisters entries we previously wrote that the registry still attributes to us.
+ * Entries overwritten by someone else are left untouched (identity check fails).
+ */
+ private function unregisterOwned(RegistryInterface $registry, DiscoveryState $obsolete): void
+ {
+ foreach ($obsolete->getTools() as $name => $owned) {
+ if ($registry->hasTool($name) && $registry->getTool($name) === $owned) {
+ $registry->unregisterTool($name);
+ }
+ }
+ foreach ($obsolete->getResources() as $uri => $owned) {
+ if ($registry->hasResource($uri) && $registry->getResource($uri, false) === $owned) {
+ $registry->unregisterResource($uri);
+ }
+ }
+ foreach ($obsolete->getResourceTemplates() as $uriTemplate => $owned) {
+ if ($registry->hasResourceTemplate($uriTemplate) && $registry->getResourceTemplate($uriTemplate) === $owned) {
+ $registry->unregisterResourceTemplate($uriTemplate);
+ }
+ }
+ foreach ($obsolete->getPrompts() as $name => $owned) {
+ if ($registry->hasPrompt($name) && $registry->getPrompt($name) === $owned) {
+ $registry->unregisterPrompt($name);
+ }
+ }
+ }
+
+ /**
+ * Writes the discovered state into the registry, skipping entries that a conflicting
+ * registration already holds. Returns the new owned state (only the writes we actually performed).
+ */
+ private function writeDiscovered(RegistryInterface $registry, DiscoveryState $discovered): DiscoveryState
+ {
+ $tools = [];
+ foreach ($discovered->getTools() as $name => $reference) {
+ if (!$this->mayWriteTool($registry, $name)) {
+ continue;
+ }
+ $tools[$name] = $registry->registerTool($reference->tool, $reference->handler);
+ }
+
+ $resources = [];
+ foreach ($discovered->getResources() as $uri => $reference) {
+ if (!$this->mayWriteResource($registry, $uri)) {
+ continue;
+ }
+ $resources[$uri] = $registry->registerResource($reference->resource, $reference->handler);
+ }
+
+ $resourceTemplates = [];
+ foreach ($discovered->getResourceTemplates() as $uriTemplate => $reference) {
+ if (!$this->mayWriteResourceTemplate($registry, $uriTemplate)) {
+ continue;
+ }
+ $resourceTemplates[$uriTemplate] = $registry->registerResourceTemplate(
+ $reference->resourceTemplate,
+ $reference->handler,
+ $reference->completionProviders,
+ );
+ }
+
+ $prompts = [];
+ foreach ($discovered->getPrompts() as $name => $reference) {
+ if (!$this->mayWritePrompt($registry, $name)) {
+ continue;
+ }
+ $prompts[$name] = $registry->registerPrompt(
+ $reference->prompt,
+ $reference->handler,
+ $reference->completionProviders,
+ );
+ }
+
+ return new DiscoveryState($tools, $resources, $prompts, $resourceTemplates);
+ }
+
+ private function mayWriteTool(RegistryInterface $registry, string $name): bool
+ {
+ if (!$registry->hasTool($name) || $registry->getTool($name) === ($this->owned->getTools()[$name] ?? null)) {
+ return true;
+ }
+
+ $this->logger->debug(\sprintf(
+ 'Ignoring discovered tool "%s": a conflicting manual or runtime registration already exists.',
+ $name,
+ ));
+
+ return false;
+ }
+
+ private function mayWriteResource(RegistryInterface $registry, string $uri): bool
+ {
+ if (!$registry->hasResource($uri) || $registry->getResource($uri, false) === ($this->owned->getResources()[$uri] ?? null)) {
+ return true;
+ }
+
+ $this->logger->debug(\sprintf(
+ 'Ignoring discovered resource "%s": a conflicting manual or runtime registration already exists.',
+ $uri,
+ ));
+
+ return false;
+ }
+
+ private function mayWriteResourceTemplate(RegistryInterface $registry, string $uriTemplate): bool
+ {
+ if (!$registry->hasResourceTemplate($uriTemplate) || $registry->getResourceTemplate($uriTemplate) === ($this->owned->getResourceTemplates()[$uriTemplate] ?? null)) {
+ return true;
+ }
+
+ $this->logger->debug(\sprintf(
+ 'Ignoring discovered resource template "%s": a conflicting manual or runtime registration already exists.',
+ $uriTemplate,
+ ));
+
+ return false;
+ }
+
+ private function mayWritePrompt(RegistryInterface $registry, string $name): bool
+ {
+ if (!$registry->hasPrompt($name) || $registry->getPrompt($name) === ($this->owned->getPrompts()[$name] ?? null)) {
+ return true;
+ }
+
+ $this->logger->debug(\sprintf(
+ 'Ignoring discovered prompt "%s": a conflicting manual or runtime registration already exists.',
+ $name,
+ ));
+
+ return false;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/ExplicitElementLoader.php b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/ExplicitElementLoader.php
new file mode 100644
index 0000000..4733ab9
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/ExplicitElementLoader.php
@@ -0,0 +1,105 @@
+
+ */
+final class ExplicitElementLoader implements LoaderInterface
+{
+ /**
+ * @param list $tools
+ * @param list $resources
+ * @param list}> $resourceTemplates
+ * @param list}> $prompts
+ */
+ public function __construct(
+ private readonly array $tools = [],
+ private readonly array $resources = [],
+ private readonly array $resourceTemplates = [],
+ private readonly array $prompts = [],
+ ) {
+ }
+
+ public function load(RegistryInterface $registry): void
+ {
+ foreach ($this->tools as $entry) {
+ $handler = $entry['handler'];
+ $registry->registerTool($entry['definition'], $this->boundClosure(
+ static function (array $arguments) use ($handler): mixed {
+ $gateway = new ClientGateway($arguments['_session']);
+ unset($arguments['_session'], $arguments['_request']);
+
+ return $handler->execute($arguments, $gateway);
+ },
+ ));
+ }
+
+ foreach ($this->resources as $entry) {
+ $handler = $entry['handler'];
+ $registry->registerResource($entry['definition'], $this->boundClosure(
+ static fn (array $arguments): mixed => $handler->read(
+ $arguments['uri'],
+ new ClientGateway($arguments['_session']),
+ ),
+ ));
+ }
+
+ foreach ($this->resourceTemplates as $entry) {
+ $handler = $entry['handler'];
+ $registry->registerResourceTemplate($entry['definition'], $this->boundClosure(
+ static function (array $arguments) use ($handler): mixed {
+ $gateway = new ClientGateway($arguments['_session']);
+ $uri = $arguments['uri'];
+ unset($arguments['_session'], $arguments['_request'], $arguments['uri']);
+
+ return $handler->read($uri, $arguments, $gateway);
+ },
+ ), $entry['completionProviders']);
+ }
+
+ foreach ($this->prompts as $entry) {
+ $handler = $entry['handler'];
+ $registry->registerPrompt($entry['definition'], $this->boundClosure(
+ static function (array $arguments) use ($handler): mixed {
+ $gateway = new ClientGateway($arguments['_session']);
+ unset($arguments['_session'], $arguments['_request']);
+
+ return $handler->get($arguments, $gateway);
+ },
+ ), $entry['completionProviders']);
+ }
+ }
+
+ private function boundClosure(\Closure $closure): \Closure
+ {
+ return \Closure::bind($closure, null, ReferenceHandler::class);
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/LoaderInterface.php b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/LoaderInterface.php
new file mode 100644
index 0000000..a7ad87c
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/LoaderInterface.php
@@ -0,0 +1,22 @@
+
+ */
+interface LoaderInterface
+{
+ public function load(RegistryInterface $registry): void;
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/ReflectedElementLoader.php b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/ReflectedElementLoader.php
new file mode 100644
index 0000000..d1e849d
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/Loader/ReflectedElementLoader.php
@@ -0,0 +1,322 @@
+
+ *
+ * @phpstan-import-type Handler from ElementReference
+ */
+final class ReflectedElementLoader implements LoaderInterface
+{
+ /**
+ * @param array{
+ * handler: Handler,
+ * name: ?string,
+ * title: ?string,
+ * description: ?string,
+ * annotations: ?ToolAnnotations,
+ * icons: ?Icon[],
+ * meta: ?array,
+ * outputSchema: ?array
+ * }[] $tools
+ * @param array{
+ * handler: Handler,
+ * uri: string,
+ * name: ?string,
+ * title: ?string,
+ * description: ?string,
+ * mimeType: ?string,
+ * size: int|null,
+ * annotations: ?Annotations,
+ * icons: ?Icon[],
+ * meta: ?array
+ * }[] $resources
+ * @param array{
+ * handler: Handler,
+ * uriTemplate: string,
+ * name: ?string,
+ * title: ?string,
+ * description: ?string,
+ * mimeType: ?string,
+ * annotations: ?Annotations,
+ * meta: ?array
+ * }[] $resourceTemplates
+ * @param array{
+ * handler: Handler,
+ * name: ?string,
+ * description: ?string,
+ * icons: ?Icon[],
+ * meta: ?array
+ * }[] $prompts
+ */
+ public function __construct(
+ private readonly array $tools = [],
+ private readonly array $resources = [],
+ private readonly array $resourceTemplates = [],
+ private readonly array $prompts = [],
+ private LoggerInterface $logger = new NullLogger(),
+ private ?SchemaGeneratorInterface $schemaGenerator = null,
+ ) {
+ }
+
+ public function load(RegistryInterface $registry): void
+ {
+ $docBlockParser = new DocBlockParser(logger: $this->logger);
+ $schemaGenerator = $this->schemaGenerator ?? new SchemaGenerator($docBlockParser);
+
+ // Register Tools
+ foreach ($this->tools as $data) {
+ try {
+ $reflection = HandlerResolver::resolve($data['handler']);
+
+ if ($reflection instanceof \ReflectionFunction) {
+ $name = $data['name'] ?? 'closure_tool_'.spl_object_id($data['handler']);
+ $description = $data['description'] ?? null;
+ } else {
+ $name = ElementMetadataResolver::resolveName($reflection, $data['name'] ?? null);
+ $description = ElementMetadataResolver::resolveDescription($reflection, $data['description'] ?? null, $docBlockParser);
+ }
+
+ $inputSchema = $data['inputSchema'] ?? $schemaGenerator->generate($reflection);
+
+ $tool = new Tool(
+ name: $name,
+ title: $data['title'] ?? null,
+ inputSchema: $inputSchema,
+ description: $description,
+ annotations: $data['annotations'] ?? null,
+ icons: $data['icons'] ?? null,
+ meta: $data['meta'] ?? null,
+ outputSchema: $data['outputSchema'] ?? null,
+ );
+ $registry->registerTool($tool, $data['handler']);
+
+ $handlerDesc = $this->getHandlerDescription($data['handler']);
+ $this->logger->debug("Registered manual tool {$name} from handler {$handlerDesc}");
+ } catch (\Throwable $e) {
+ $this->logger->error(
+ 'Failed to register manual tool',
+ ['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e],
+ );
+ throw new ConfigurationException("Error registering manual tool '{$data['name']}': {$e->getMessage()}", 0, $e);
+ }
+ }
+
+ // Register Resources
+ foreach ($this->resources as $data) {
+ try {
+ $reflection = HandlerResolver::resolve($data['handler']);
+
+ if ($reflection instanceof \ReflectionFunction) {
+ $name = $data['name'] ?? 'closure_resource_'.spl_object_id($data['handler']);
+ $description = $data['description'] ?? null;
+ } else {
+ $name = ElementMetadataResolver::resolveName($reflection, $data['name'] ?? null);
+ $description = ElementMetadataResolver::resolveDescription($reflection, $data['description'] ?? null, $docBlockParser);
+ }
+
+ $resource = new ResourceDefinition(
+ uri: $data['uri'],
+ name: $name,
+ title: $data['title'] ?? null,
+ description: $description,
+ mimeType: $data['mimeType'] ?? null,
+ annotations: $data['annotations'] ?? null,
+ size: $data['size'] ?? null,
+ icons: $data['icons'] ?? null,
+ meta: $data['meta'] ?? null,
+ );
+ $registry->registerResource($resource, $data['handler']);
+
+ $handlerDesc = $this->getHandlerDescription($data['handler']);
+ $this->logger->debug("Registered manual resource {$name} from handler {$handlerDesc}");
+ } catch (\Throwable $e) {
+ $this->logger->error(
+ 'Failed to register manual resource',
+ ['handler' => $data['handler'], 'uri' => $data['uri'], 'exception' => $e],
+ );
+ throw new ConfigurationException("Error registering manual resource '{$data['uri']}': {$e->getMessage()}", 0, $e);
+ }
+ }
+
+ // Register Templates
+ foreach ($this->resourceTemplates as $data) {
+ try {
+ $reflection = HandlerResolver::resolve($data['handler']);
+
+ if ($reflection instanceof \ReflectionFunction) {
+ $name = $data['name'] ?? 'closure_template_'.spl_object_id($data['handler']);
+ $description = $data['description'] ?? null;
+ } else {
+ $name = ElementMetadataResolver::resolveName($reflection, $data['name'] ?? null);
+ $description = ElementMetadataResolver::resolveDescription($reflection, $data['description'] ?? null, $docBlockParser);
+ }
+
+ $template = new ResourceTemplate(
+ uriTemplate: $data['uriTemplate'],
+ name: $name,
+ title: $data['title'] ?? null,
+ description: $description,
+ mimeType: $data['mimeType'] ?? null,
+ annotations: $data['annotations'] ?? null,
+ meta: $data['meta'] ?? null,
+ );
+ $completionProviders = $this->getCompletionProviders($reflection);
+ $registry->registerResourceTemplate($template, $data['handler'], $completionProviders);
+
+ $handlerDesc = $this->getHandlerDescription($data['handler']);
+ $this->logger->debug("Registered manual template {$name} from handler {$handlerDesc}");
+ } catch (\Throwable $e) {
+ $this->logger->error(
+ 'Failed to register manual template',
+ ['handler' => $data['handler'], 'uriTemplate' => $data['uriTemplate'], 'exception' => $e],
+ );
+ throw new ConfigurationException("Error registering manual resource template '{$data['uriTemplate']}': {$e->getMessage()}", 0, $e);
+ }
+ }
+
+ // Register Prompts
+ foreach ($this->prompts as $data) {
+ try {
+ $reflection = HandlerResolver::resolve($data['handler']);
+
+ if ($reflection instanceof \ReflectionFunction) {
+ $name = $data['name'] ?? 'closure_prompt_'.spl_object_id($data['handler']);
+ $description = $data['description'] ?? null;
+ } else {
+ $name = ElementMetadataResolver::resolveName($reflection, $data['name'] ?? null);
+ $description = ElementMetadataResolver::resolveDescription($reflection, $data['description'] ?? null, $docBlockParser);
+ }
+
+ $arguments = [];
+ $paramTags = $reflection instanceof \ReflectionMethod ? $docBlockParser->getParamTags(
+ $docBlockParser->parseDocBlock($reflection->getDocComment() ?? null),
+ ) : [];
+ foreach ($reflection->getParameters() as $param) {
+ $reflectionType = $param->getType();
+
+ // Basic DI check (heuristic)
+ if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
+ continue;
+ }
+
+ $paramTag = $paramTags['$'.$param->getName()] ?? null;
+ $arguments[] = new PromptArgument(
+ $param->getName(),
+ $paramTag ? trim((string) $paramTag->getDescription()) : null,
+ !$param->isOptional() && !$param->isDefaultValueAvailable(),
+ );
+ }
+ $prompt = new Prompt(
+ name: $name,
+ title: $data['title'] ?? null,
+ description: $description,
+ arguments: $arguments,
+ icons: $data['icons'] ?? null,
+ meta: $data['meta'] ?? null
+ );
+ $completionProviders = $this->getCompletionProviders($reflection);
+ $registry->registerPrompt($prompt, $data['handler'], $completionProviders);
+
+ $handlerDesc = $this->getHandlerDescription($data['handler']);
+ $this->logger->debug("Registered manual prompt {$name} from handler {$handlerDesc}");
+ } catch (\Throwable $e) {
+ $this->logger->error(
+ 'Failed to register manual prompt',
+ ['handler' => $data['handler'], 'name' => $data['name'], 'exception' => $e],
+ );
+ throw new ConfigurationException("Error registering manual prompt '{$data['name']}': {$e->getMessage()}", 0, $e);
+ }
+ }
+
+ $this->logger->debug('Manual element registration complete.');
+ }
+
+ /**
+ * @param Handler $handler
+ */
+ private function getHandlerDescription(\Closure|array|string $handler): string
+ {
+ if ($handler instanceof \Closure) {
+ return 'Closure';
+ }
+
+ if (\is_array($handler)) {
+ return \sprintf(
+ '%s::%s',
+ \is_object($handler[0]) ? $handler[0]::class : $handler[0],
+ $handler[1],
+ );
+ }
+
+ return (string) $handler;
+ }
+
+ /**
+ * @return array
+ */
+ private function getCompletionProviders(\ReflectionMethod|\ReflectionFunction $reflection): array
+ {
+ $completionProviders = [];
+ foreach ($reflection->getParameters() as $param) {
+ $reflectionType = $param->getType();
+ if ($reflectionType instanceof \ReflectionNamedType && !$reflectionType->isBuiltin()) {
+ continue;
+ }
+
+ $completionAttributes = $param->getAttributes(
+ CompletionProvider::class,
+ \ReflectionAttribute::IS_INSTANCEOF,
+ );
+ if (!empty($completionAttributes)) {
+ $attributeInstance = $completionAttributes[0]->newInstance();
+
+ if ($attributeInstance->provider) {
+ $completionProviders[$param->getName()] = $attributeInstance->provider;
+ } elseif ($attributeInstance->providerClass) {
+ $completionProviders[$param->getName()] = $attributeInstance->providerClass;
+ } elseif ($attributeInstance->values) {
+ $completionProviders[$param->getName()] = new ListCompletionProvider($attributeInstance->values);
+ } elseif ($attributeInstance->enum) {
+ $completionProviders[$param->getName()] = new EnumCompletionProvider($attributeInstance->enum);
+ }
+ }
+ }
+
+ return $completionProviders;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/PromptReference.php b/admin/vendor/mcp/sdk/src/Capability/Registry/PromptReference.php
new file mode 100644
index 0000000..ec7d021
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/PromptReference.php
@@ -0,0 +1,51 @@
+
+ */
+class PromptReference extends ElementReference
+{
+ /**
+ * @param Handler $handler
+ * @param array $completionProviders
+ */
+ public function __construct(
+ public readonly Prompt $prompt,
+ \Closure|array|string $handler,
+ public readonly array $completionProviders = [],
+ ) {
+ parent::__construct($handler);
+ }
+
+ /**
+ * Formats the raw result of a prompt generator into an array of MCP PromptMessages.
+ *
+ * @param mixed $promptGenerationResult expected: array of message structures
+ *
+ * @return PromptMessage[] array of PromptMessage objects
+ *
+ * @throws \RuntimeException if the result cannot be formatted
+ * @throws \JsonException if JSON encoding fails
+ */
+ public function formatResult(mixed $promptGenerationResult): array
+ {
+ return (new PromptResultFormatter())->format($promptGenerationResult);
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/ReferenceHandler.php b/admin/vendor/mcp/sdk/src/Capability/Registry/ReferenceHandler.php
new file mode 100644
index 0000000..c0084cb
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/ReferenceHandler.php
@@ -0,0 +1,311 @@
+
+ */
+final class ReferenceHandler implements ReferenceHandlerInterface
+{
+ public function __construct(
+ private readonly ?ContainerInterface $container = null,
+ ) {
+ }
+
+ /**
+ * @param array $arguments
+ */
+ public function handle(ElementReference $reference, array $arguments): mixed
+ {
+ // Closures bound to this class as their scope consume the raw argument bag
+ // directly. Used by ExplicitElementLoader so reflection + name-based parameter
+ // mapping is bypassed for explicitly registered handler interfaces.
+ if ($reference->handler instanceof \Closure
+ && self::class === (new \ReflectionFunction($reference->handler))->getClosureScopeClass()?->getName()
+ ) {
+ return ($reference->handler)($arguments);
+ }
+
+ $session = $arguments['_session'];
+
+ if (\is_string($reference->handler)) {
+ if (class_exists($reference->handler) && method_exists($reference->handler, '__invoke')) {
+ $reflection = new \ReflectionMethod($reference->handler, '__invoke');
+ $instance = $this->getClassInstance($reference->handler);
+ $arguments = $this->prepareArguments($reflection, $arguments);
+
+ return \call_user_func($instance, ...$arguments);
+ }
+
+ if (\function_exists($reference->handler)) {
+ $reflection = new \ReflectionFunction($reference->handler);
+ $arguments = $this->prepareArguments($reflection, $arguments);
+
+ return \call_user_func($reference->handler, ...$arguments);
+ }
+ }
+
+ if (\is_callable($reference->handler)) {
+ $reflection = $this->getReflectionForCallable($reference->handler, $session);
+ $arguments = $this->prepareArguments($reflection, $arguments);
+
+ return \call_user_func($reference->handler, ...$arguments);
+ }
+
+ if (\is_array($reference->handler)) {
+ [$className, $methodName] = $reference->handler;
+ $reflection = new \ReflectionMethod($className, $methodName);
+ $instance = $this->getClassInstance($className);
+ $arguments = $this->prepareArguments($reflection, $arguments);
+
+ return \call_user_func([$instance, $methodName], ...$arguments);
+ }
+
+ throw new InvalidArgumentException('Invalid handler type');
+ }
+
+ private function getClassInstance(string $className): object
+ {
+ if (null !== $this->container && $this->container->has($className)) {
+ return $this->container->get($className);
+ }
+
+ return new $className();
+ }
+
+ /**
+ * @param array $arguments
+ *
+ * @return array
+ */
+ private function prepareArguments(\ReflectionFunctionAbstract $reflection, array $arguments): array
+ {
+ $finalArgs = [];
+
+ foreach ($reflection->getParameters() as $parameter) {
+ $paramName = $parameter->getName();
+ $paramPosition = $parameter->getPosition();
+
+ if ($parameter->isVariadic()) {
+ // Each element is cast individually below; a non-array value is
+ // wrapped as a single element, since prompt arguments arrive as
+ // plain strings (Record per the protocol) even
+ // though tool arguments follow the advertised "array" schema.
+ $values = $arguments[$paramName] ?? [];
+ if (!\is_array($values)) {
+ $values = [$values];
+ } elseif (!array_is_list($values)) {
+ throw RegistryException::invalidParams(\sprintf('Parameter `%s` must be a list of values, not an object.', $paramName));
+ }
+ foreach ($values as $index => $value) {
+ try {
+ $finalArgs[] = $this->castArgumentType($value, $parameter);
+ } catch (InvalidArgumentException $e) {
+ throw RegistryException::invalidParams(\sprintf('Parameter `%s[%d]`: %s', $paramName, $index, $e->getMessage()), $e);
+ } catch (\Throwable $e) {
+ throw RegistryException::internalError(\sprintf('Error processing parameter `%s[%d]`: %s', $paramName, $index, $e->getMessage()), $e);
+ }
+ }
+ continue;
+ }
+
+ // Check if parameter is a special injectable type
+ $type = $parameter->getType();
+ if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
+ $injected = InjectableParameters::resolve($type->getName(), $arguments);
+ if (null !== $injected) {
+ $finalArgs[$paramPosition] = $injected;
+ continue;
+ }
+ }
+
+ if (isset($arguments[$paramName])) {
+ $argument = $arguments[$paramName];
+ try {
+ $finalArgs[$paramPosition] = $this->castArgumentType($argument, $parameter);
+ } catch (InvalidArgumentException $e) {
+ throw RegistryException::invalidParams($e->getMessage(), $e);
+ } catch (\Throwable $e) {
+ throw RegistryException::internalError("Error processing parameter `{$paramName}`: {$e->getMessage()}", $e);
+ }
+ } elseif ($parameter->isDefaultValueAvailable()) {
+ $finalArgs[$paramPosition] = $parameter->getDefaultValue();
+ } elseif ($parameter->allowsNull()) {
+ $finalArgs[$paramPosition] = null;
+ } elseif ($parameter->isOptional()) {
+ continue;
+ } else {
+ $reflectionName = $reflection instanceof \ReflectionMethod
+ ? $reflection->class.'::'.$reflection->name
+ : 'Closure';
+ throw RegistryException::internalError("Missing required argument `{$paramName}` for {$reflectionName}.");
+ }
+ }
+
+ return array_values($finalArgs);
+ }
+
+ /**
+ * Gets a ReflectionMethod or ReflectionFunction for a callable.
+ */
+ private function getReflectionForCallable(callable $handler, SessionInterface $session): \ReflectionMethod|\ReflectionFunction
+ {
+ if (\is_string($handler)) {
+ return new \ReflectionFunction($handler);
+ }
+
+ if ($handler instanceof \Closure) {
+ return new \ReflectionFunction($handler);
+ }
+
+ if (\is_array($handler) && 2 === \count($handler)) {
+ [$class, $method] = $handler;
+
+ return new \ReflectionMethod($class, $method);
+ }
+
+ throw new InvalidArgumentException('Cannot create reflection for this callable type');
+ }
+
+ /**
+ * Attempts type casting based on ReflectionParameter type hints.
+ *
+ * @throws InvalidArgumentException if casting is impossible for the required type
+ */
+ private function castArgumentType(mixed $argument, \ReflectionParameter $parameter): mixed
+ {
+ $type = $parameter->getType();
+
+ if (null === $argument) {
+ if ($type && $type->allowsNull()) {
+ return null;
+ }
+ }
+
+ if (!$type instanceof \ReflectionNamedType) {
+ return $argument;
+ }
+
+ $typeName = $type->getName();
+
+ if (enum_exists($typeName)) {
+ if (\is_object($argument) && $argument instanceof $typeName) {
+ return $argument;
+ }
+
+ if (is_subclass_of($typeName, \BackedEnum::class)) {
+ $value = $typeName::tryFrom($argument);
+ if (null === $value) {
+ throw new InvalidArgumentException("Invalid value '{$argument}' for backed enum {$typeName}. Expected one of its backing values.");
+ }
+
+ return $value;
+ }
+ if (\is_string($argument)) {
+ foreach ($typeName::cases() as $case) {
+ if ($case->name === $argument) {
+ return $case;
+ }
+ }
+ $validNames = array_map(static fn ($c) => $c->name, $typeName::cases());
+ throw new InvalidArgumentException("Invalid value '{$argument}' for unit enum {$typeName}. Expected one of: ".implode(', ', $validNames).'.');
+ }
+ throw new InvalidArgumentException("Invalid value type '{$argument}' for unit enum {$typeName}. Expected a string matching a case name.");
+ }
+
+ try {
+ return match (strtolower($typeName)) {
+ 'int', 'integer' => $this->castToInt($argument),
+ 'string' => (string) $argument,
+ 'bool', 'boolean' => $this->castToBoolean($argument),
+ 'float', 'double' => $this->castToFloat($argument),
+ 'array' => $this->castToArray($argument),
+ default => $argument,
+ };
+ } catch (\TypeError $e) {
+ throw new InvalidArgumentException("Value cannot be cast to required type `{$typeName}`.", 0, $e);
+ }
+ }
+
+ /**
+ * Helper to cast strictly to boolean.
+ */
+ private function castToBoolean(mixed $argument): bool
+ {
+ if (\is_bool($argument)) {
+ return $argument;
+ }
+ if (1 === $argument || '1' === $argument || 'true' === strtolower((string) $argument)) {
+ return true;
+ }
+ if (0 === $argument || '0' === $argument || 'false' === strtolower((string) $argument)) {
+ return false;
+ }
+
+ throw new InvalidArgumentException('Cannot cast value to boolean. Use true/false/1/0.');
+ }
+
+ /**
+ * Helper to cast strictly to integer.
+ */
+ private function castToInt(mixed $argument): int
+ {
+ if (\is_int($argument)) {
+ return $argument;
+ }
+ if (is_numeric($argument) && floor((float) $argument) == $argument && !\is_string($argument)) {
+ return (int) $argument;
+ }
+ if (\is_string($argument) && ctype_digit(ltrim($argument, '-'))) {
+ return (int) $argument;
+ }
+
+ throw new InvalidArgumentException('Cannot cast value to integer. Expected integer representation.');
+ }
+
+ /**
+ * Helper to cast strictly to float.
+ */
+ private function castToFloat(mixed $argument): float
+ {
+ if (\is_float($argument)) {
+ return $argument;
+ }
+ if (\is_int($argument)) {
+ return (float) $argument;
+ }
+ if (is_numeric($argument)) {
+ return (float) $argument;
+ }
+
+ throw new InvalidArgumentException('Cannot cast value to float. Expected numeric representation.');
+ }
+
+ /**
+ * Helper to cast strictly to array.
+ *
+ * @return array
+ */
+ private function castToArray(mixed $argument): array
+ {
+ if (\is_array($argument)) {
+ return $argument;
+ }
+
+ throw new InvalidArgumentException('Cannot cast value to array. Expected array.');
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/ReferenceHandlerInterface.php b/admin/vendor/mcp/sdk/src/Capability/Registry/ReferenceHandlerInterface.php
new file mode 100644
index 0000000..c4e52f1
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/ReferenceHandlerInterface.php
@@ -0,0 +1,37 @@
+
+ */
+interface ReferenceHandlerInterface
+{
+ /**
+ * Handles execution of an MCP element reference.
+ *
+ * @param ElementReference $reference the element reference to execute
+ * @param array $arguments arguments to pass to the handler
+ *
+ * @return mixed the result of the element execution
+ *
+ * @throws InvalidArgumentException if the handler is invalid
+ * @throws RegistryException if execution fails
+ */
+ public function handle(ElementReference $reference, array $arguments): mixed;
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/ResourceReference.php b/admin/vendor/mcp/sdk/src/Capability/Registry/ResourceReference.php
new file mode 100644
index 0000000..10d7837
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/ResourceReference.php
@@ -0,0 +1,59 @@
+
+ */
+class ResourceReference extends ElementReference
+{
+ /**
+ * @param Handler $handler
+ */
+ public function __construct(
+ public readonly ResourceDefinition $resource,
+ callable|array|string $handler,
+ ) {
+ parent::__construct($handler);
+ }
+
+ /**
+ * Formats the raw result of a resource read operation into MCP ResourceContent items.
+ *
+ * @param mixed $readResult the raw result from the resource handler method
+ * @param string $uri the URI of the resource that was read
+ * @param ?string $mimeType the MIME type from the ResourceDefinition
+ *
+ * @return ResourceContents[] array of ResourceContents objects
+ *
+ * Supported result types:
+ * - ResourceContents: Used as-is
+ * - EmbeddedResource: Resource is extracted from the EmbeddedResource
+ * - string: Converted to text content with guessed or provided MIME type
+ * - stream resource: Read and converted to blob with provided MIME type
+ * - array with 'blob' key: Used as blob content
+ * - array with 'text' key: Used as text content
+ * - SplFileInfo: Read and converted to blob
+ * - array: Converted to JSON if MIME type is application/json or contains 'json'
+ * For other MIME types, will try to convert to JSON with a warning
+ */
+ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = null): array
+ {
+ return (new ResourceResultFormatter())->format($readResult, $uri, $mimeType, $this->resource->meta);
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/ResourceTemplateReference.php b/admin/vendor/mcp/sdk/src/Capability/Registry/ResourceTemplateReference.php
new file mode 100644
index 0000000..49d03b3
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/ResourceTemplateReference.php
@@ -0,0 +1,112 @@
+
+ */
+class ResourceTemplateReference extends ElementReference
+{
+ /**
+ * @var array
+ */
+ private array $variableNames;
+
+ private string $uriTemplateRegex;
+
+ /**
+ * @param Handler $handler
+ * @param array $completionProviders
+ */
+ public function __construct(
+ public readonly ResourceTemplate $resourceTemplate,
+ callable|array|string $handler,
+ public readonly array $completionProviders = [],
+ ) {
+ parent::__construct($handler);
+
+ $this->compileTemplate();
+ }
+
+ /**
+ * @return array
+ */
+ public function getVariableNames(): array
+ {
+ return $this->variableNames;
+ }
+
+ public function matches(string $uri): bool
+ {
+ return 1 === preg_match($this->uriTemplateRegex, $uri);
+ }
+
+ /** @return array */
+ public function extractVariables(string $uri): array
+ {
+ $matches = [];
+
+ preg_match($this->uriTemplateRegex, $uri, $matches);
+
+ return array_filter($matches, fn ($key) => \in_array($key, $this->variableNames), \ARRAY_FILTER_USE_KEY);
+ }
+
+ /**
+ * Formats the raw result of a resource read operation into MCP ResourceContent items.
+ *
+ * @param mixed $readResult the raw result from the resource handler method
+ * @param string $uri the URI of the resource that was read
+ *
+ * @return array array of ResourceContents objects
+ *
+ * Supported result types:
+ * - ResourceContents: Used as-is
+ * - EmbeddedResource: Resource is extracted from the EmbeddedResource
+ * - string: Converted to text content with guessed or provided MIME type
+ * - stream resource: Read and converted to blob with provided MIME type
+ * - array with 'blob' key: Used as blob content
+ * - array with 'text' key: Used as text content
+ * - SplFileInfo: Read and converted to blob
+ * - array: Converted to JSON if MIME type is application/json or contains 'json'
+ * For other MIME types, will try to convert to JSON with a warning
+ */
+ public function formatResult(mixed $readResult, string $uri, ?string $mimeType = null): array
+ {
+ return (new ResourceResultFormatter())->format($readResult, $uri, $mimeType, $this->resourceTemplate->meta);
+ }
+
+ private function compileTemplate(): void
+ {
+ $this->variableNames = [];
+ $regexParts = [];
+
+ $segments = preg_split('/(\{\w+\})/', $this->resourceTemplate->uriTemplate, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
+
+ foreach ($segments as $segment) {
+ if (preg_match('/^\{(\w+)\}$/', $segment, $matches)) {
+ $varName = $matches[1];
+ $this->variableNames[] = $varName;
+ $regexParts[] = '(?P<'.$varName.'>[^/]+)';
+ } else {
+ $regexParts[] = preg_quote($segment, '#');
+ }
+ }
+
+ $this->uriTemplateRegex = '#^'.implode('', $regexParts).'$#';
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Registry/ToolReference.php b/admin/vendor/mcp/sdk/src/Capability/Registry/ToolReference.php
new file mode 100644
index 0000000..97ace43
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Registry/ToolReference.php
@@ -0,0 +1,142 @@
+
+ */
+class ToolReference extends ElementReference
+{
+ /**
+ * @param Handler $handler
+ */
+ public function __construct(
+ public readonly Tool $tool,
+ callable|array|string $handler,
+ ) {
+ parent::__construct($handler);
+ }
+
+ /**
+ * Formats the result of a tool execution into an array of MCP Content items.
+ *
+ * - If the result is already a Content object, it's wrapped in an array.
+ * - If the result is an array:
+ * - If all elements are Content objects, the array is returned as is.
+ * - If it's a mixed array (Content and non-Content items), non-Content items are
+ * individually formatted (scalars to TextContent, others to JSON TextContent).
+ * - If it's an array with no Content items, the entire array is JSON-encoded into a single TextContent.
+ * - Scalars (string, int, float, bool) are wrapped in TextContent.
+ * - null is represented as TextContent('(null)').
+ * - Other objects are JSON-encoded and wrapped in TextContent.
+ *
+ * @param mixed $toolExecutionResult the raw value returned by the tool's PHP method
+ *
+ * @return Content[] the content items for CallToolResult
+ *
+ * @throws \JsonException if JSON encoding fails for non-Content array/object results
+ */
+ public function formatResult(mixed $toolExecutionResult): array
+ {
+ return (new ToolResultFormatter())->format($toolExecutionResult);
+ }
+
+ /**
+ * Extracts structured content from a tool result using the output schema.
+ *
+ * What may be sent as `structuredContent` depends on the protocol revision in
+ * use. Up to `2025-11-25` it has to be a JSON object, and `outputSchema` is
+ * restricted to `type: "object"` to match. From `2026-07-28` on (SEP-2106)
+ * `outputSchema` is any JSON Schema 2020-12 and `structuredContent` is any JSON
+ * value conforming to it — a list included.
+ *
+ * @param mixed $toolExecutionResult the raw value returned by the tool's PHP method
+ * @param ?ProtocolVersion $protocolVersion revision the result is produced for; defaults to the
+ * newest handshake revision, whose stricter rule is what
+ * every revision reachable through `initialize` requires
+ *
+ * @return mixed the structured content, or null if not extractable
+ *
+ * @throws \JsonException if JSON encoding fails for non-Content array/object results
+ */
+ public function extractStructuredContent(mixed $toolExecutionResult, ?ProtocolVersion $protocolVersion = null): mixed
+ {
+ $objectOnly = ($protocolVersion ?? ProtocolVersion::latestHandshake())->requiresObjectStructuredContent();
+
+ if (\is_array($toolExecutionResult)) {
+ // A PHP list serializes to a JSON array, which the revisions predating
+ // SEP-2106 do not allow as `structuredContent` — strict clients reject
+ // the whole tool call when one is sent.
+ if ($objectOnly && array_is_list($toolExecutionResult)) {
+ return null;
+ }
+
+ foreach ($toolExecutionResult as $item) {
+ if ($item instanceof Content) {
+ // Content items are already reflected in the result's `content`
+ // array; an array holding one or more of them isn't structured
+ // data. This holds in every revision — it is a duplication rule,
+ // not a shape rule.
+ return null;
+ }
+ }
+
+ return $toolExecutionResult;
+ }
+
+ if (\is_object($toolExecutionResult) && !($toolExecutionResult instanceof Content)) {
+ $jsonResult = json_encode(
+ $toolExecutionResult,
+ \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR | \JSON_INVALID_UTF8_SUBSTITUTE
+ );
+
+ $decoded = json_decode(
+ $jsonResult, true, 512, \JSON_THROW_ON_ERROR
+ );
+
+ // A plain object always encodes to a JSON object, but `JsonSerializable`
+ // can hand back anything, scalars included.
+ if (!\is_array($decoded)) {
+ return $this->acceptsScalarStructuredContent($objectOnly) ? $decoded : null;
+ }
+
+ if ($objectOnly && array_is_list($decoded)) {
+ return null;
+ }
+
+ return $decoded;
+ }
+
+ // A scalar is structured content only from SEP-2106 on, and only when the
+ // tool declared an outputSchema: without one, every string-returning tool
+ // would start advertising a duplicate of its own `content`.
+ return $this->acceptsScalarStructuredContent($objectOnly) && \is_scalar($toolExecutionResult)
+ ? $toolExecutionResult
+ : null;
+ }
+
+ /**
+ * Whether the negotiated revision and the tool's own declaration together allow
+ * a non-object `structuredContent`.
+ */
+ private function acceptsScalarStructuredContent(bool $objectOnly): bool
+ {
+ return !$objectOnly && null !== $this->tool->outputSchema;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/RegistryInterface.php b/admin/vendor/mcp/sdk/src/Capability/RegistryInterface.php
new file mode 100644
index 0000000..bbbef76
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/RegistryInterface.php
@@ -0,0 +1,175 @@
+
+ * @author Christopher Hertel
+ */
+interface RegistryInterface
+{
+ /**
+ * Registers a tool with its handler. Overwrites any prior registration of the same name.
+ * Returns the stored reference, whose identity callers may track to detect later overwrites.
+ *
+ * @param Handler $handler
+ */
+ public function registerTool(Tool $tool, callable|array|string $handler): ToolReference;
+
+ /**
+ * Registers a resource with its handler. Overwrites any prior registration of the same URI.
+ * Returns the stored reference, whose identity callers may track to detect later overwrites.
+ *
+ * @param Handler $handler
+ */
+ public function registerResource(ResourceDefinition $resource, callable|array|string $handler): ResourceReference;
+
+ /**
+ * Registers a resource template with its handler and completion providers.
+ * Overwrites any prior registration of the same URI template.
+ * Returns the stored reference, whose identity callers may track to detect later overwrites.
+ *
+ * @param Handler $handler
+ * @param array $completionProviders
+ */
+ public function registerResourceTemplate(
+ ResourceTemplate $template,
+ callable|array|string $handler,
+ array $completionProviders = [],
+ ): ResourceTemplateReference;
+
+ /**
+ * Registers a prompt with its handler and completion providers.
+ * Overwrites any prior registration of the same name.
+ * Returns the stored reference, whose identity callers may track to detect later overwrites.
+ *
+ * @param Handler $handler
+ * @param array $completionProviders
+ */
+ public function registerPrompt(
+ Prompt $prompt,
+ callable|array|string $handler,
+ array $completionProviders = [],
+ ): PromptReference;
+
+ /**
+ * Removes a tool by name. No-op if absent.
+ */
+ public function unregisterTool(string $name): void;
+
+ /**
+ * Removes a resource by URI. No-op if absent.
+ */
+ public function unregisterResource(string $uri): void;
+
+ /**
+ * Removes a resource template by URI template. No-op if absent.
+ */
+ public function unregisterResourceTemplate(string $uriTemplate): void;
+
+ /**
+ * Removes a prompt by name. No-op if absent.
+ */
+ public function unregisterPrompt(string $name): void;
+
+ public function hasTool(string $name): bool;
+
+ public function hasResource(string $uri): bool;
+
+ public function hasResourceTemplate(string $uriTemplate): bool;
+
+ public function hasPrompt(string $name): bool;
+
+ /**
+ * @return bool true if any tools are registered
+ */
+ public function hasTools(): bool;
+
+ /**
+ * Gets all registered tools.
+ */
+ public function getTools(?int $limit = null, ?string $cursor = null): Page;
+
+ /**
+ * Gets a tool reference by name.
+ *
+ * @throws ToolNotFoundException
+ */
+ public function getTool(string $name): ToolReference;
+
+ /**
+ * @return bool true if any resources are registered
+ */
+ public function hasResources(): bool;
+
+ /**
+ * Gets all registered resources.
+ */
+ public function getResources(?int $limit = null, ?string $cursor = null): Page;
+
+ /**
+ * Gets a resource reference by URI (includes template matching if enabled).
+ *
+ * @throws ResourceNotFoundException
+ */
+ public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference;
+
+ /**
+ * @return bool true if any resource templates are registered
+ */
+ public function hasResourceTemplates(): bool;
+
+ /**
+ * Gets all registered resource templates.
+ */
+ public function getResourceTemplates(?int $limit = null, ?string $cursor = null): Page;
+
+ /**
+ * Gets a resource template reference by URI template.
+ *
+ * @throws ResourceNotFoundException
+ */
+ public function getResourceTemplate(string $uriTemplate): ResourceTemplateReference;
+
+ /**
+ * @return bool true if any prompts are registered
+ */
+ public function hasPrompts(): bool;
+
+ /**
+ * Gets all registered prompts.
+ */
+ public function getPrompts(?int $limit = null, ?string $cursor = null): Page;
+
+ /**
+ * Gets a prompt reference by name.
+ *
+ * @throws PromptNotFoundException
+ */
+ public function getPrompt(string $name): PromptReference;
+}
diff --git a/admin/vendor/mcp/sdk/src/Capability/Tool/NameValidator.php b/admin/vendor/mcp/sdk/src/Capability/Tool/NameValidator.php
new file mode 100644
index 0000000..fe9bb08
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Capability/Tool/NameValidator.php
@@ -0,0 +1,20 @@
+
+ */
+class Client
+{
+ private const RETRY_BASE_DELAY_MS = 100;
+
+ private ?TransportInterface $transport = null;
+
+ public function __construct(
+ private readonly Protocol $protocol,
+ private readonly Configuration $config,
+ private readonly LoggerInterface $logger = new NullLogger(),
+ ) {
+ }
+
+ /**
+ * Create a new client builder for fluent configuration.
+ */
+ public static function builder(): Builder
+ {
+ return new Builder();
+ }
+
+ /**
+ * Connect to an MCP server using the provided transport.
+ *
+ * A failed attempt is closed and retried, see {@see Builder::setMaxRetries()}.
+ *
+ * @throws ConnectionException If connection or initialization fails on every attempt
+ */
+ public function connect(TransportInterface $transport): void
+ {
+ $this->transport = $transport;
+ $this->protocol->connect($transport, $this->config);
+
+ $maxAttempts = $this->config->maxRetries + 1;
+
+ for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) {
+ try {
+ $transport->connect();
+
+ $this->logger->info('Client connected and initialized', ['attempt' => $attempt]);
+
+ return;
+ } catch (ConnectionException $e) {
+ // initialize() flags the session before sending the initialized
+ // notification, so a failure in between leaves the flag set.
+ $this->protocol->getState()->setInitialized(false);
+
+ $transport->close();
+
+ if ($attempt === $maxAttempts) {
+ throw $e;
+ }
+
+ $this->logger->warning('Connection attempt failed, retrying', [
+ 'attempt' => $attempt,
+ 'max_attempts' => $maxAttempts,
+ 'exception' => $e,
+ ]);
+
+ usleep($attempt * self::RETRY_BASE_DELAY_MS * 1000);
+ }
+ }
+ }
+
+ /**
+ * Check if connected and initialized.
+ */
+ public function isConnected(): bool
+ {
+ return null !== $this->transport && $this->protocol->getState()->isInitialized();
+ }
+
+ /**
+ * Get server information from initialization.
+ */
+ public function getServerInfo(): ?Implementation
+ {
+ return $this->protocol->getState()->getServerInfo();
+ }
+
+ /**
+ * Get server instructions.
+ */
+ public function getInstructions(): ?string
+ {
+ return $this->protocol->getState()->getInstructions();
+ }
+
+ /**
+ * Protocol revision negotiated during the handshake.
+ *
+ * This is the version the server answered with, which is not necessarily the
+ * one configured on the builder: a server that cannot speak the requested
+ * revision counter-offers one it supports. Null until the handshake completed.
+ */
+ public function getProtocolVersion(): ?ProtocolVersion
+ {
+ return $this->protocol->getState()->getProtocolVersion();
+ }
+
+ /**
+ * Send a ping request to the server.
+ */
+ public function ping(): void
+ {
+ $request = new PingRequest();
+
+ $this->sendRequest($request);
+ }
+
+ /**
+ * List available tools from the server.
+ */
+ public function listTools(?string $cursor = null): ListToolsResult
+ {
+ $request = new ListToolsRequest($cursor);
+
+ $response = $this->sendRequest($request);
+
+ $result = $response->result;
+
+ // Filtered before parsing, because parsing is where a malformed
+ // `x-mcp-header` annotation throws. One broken definition must cost the
+ // caller that tool, not the whole listing (SEP-2243).
+ if (\is_array($result['tools'] ?? null)) {
+ $result['tools'] = $this->protocol->getToolCatalog()->record($result['tools']);
+ }
+
+ return ListToolsResult::fromArray($result);
+ }
+
+ /**
+ * Call a tool on the server.
+ *
+ * @param string $name Tool name
+ * @param array $arguments Tool arguments
+ * @param (callable(float $progress, ?float $total, ?string $message): void)|null $onProgress
+ * Optional callback for progress updates
+ */
+ public function callTool(string $name, array $arguments = [], ?callable $onProgress = null): CallToolResult
+ {
+ $catalog = $this->protocol->getToolCatalog();
+
+ // A tool the listing showed to be malformed is refused here rather than
+ // sent: the client cannot produce the headers its annotations demand,
+ // so the call could only go out misdescribed (SEP-2243).
+ if ($catalog->isRejected($name)) {
+ throw new RuntimeException(\sprintf('Refusing to call tool "%s": its "x-mcp-header" annotations are invalid (%s).', $name, $catalog->reasonFor($name)));
+ }
+
+ $request = new CallToolRequest($name, $arguments);
+
+ $response = $this->sendRequest($request, $onProgress);
+
+ return CallToolResult::fromArray($response->result);
+ }
+
+ /**
+ * List available resources from the server.
+ */
+ public function listResources(?string $cursor = null): ListResourcesResult
+ {
+ $request = new ListResourcesRequest($cursor);
+
+ $response = $this->sendRequest($request);
+
+ return ListResourcesResult::fromArray($response->result);
+ }
+
+ /**
+ * List available resource templates from the server.
+ */
+ public function listResourceTemplates(?string $cursor = null): ListResourceTemplatesResult
+ {
+ $request = new ListResourceTemplatesRequest($cursor);
+
+ $response = $this->sendRequest($request);
+
+ return ListResourceTemplatesResult::fromArray($response->result);
+ }
+
+ /**
+ * Read a resource by URI.
+ *
+ * @param string $uri The resource URI
+ * @param (callable(float $progress, ?float $total, ?string $message): void)|null $onProgress
+ * Optional callback for progress updates
+ */
+ public function readResource(string $uri, ?callable $onProgress = null): ReadResourceResult
+ {
+ $request = new ReadResourceRequest($uri);
+
+ $response = $this->sendRequest($request, $onProgress);
+
+ return ReadResourceResult::fromArray($response->result);
+ }
+
+ /**
+ * List available prompts from the server.
+ */
+ public function listPrompts(?string $cursor = null): ListPromptsResult
+ {
+ $request = new ListPromptsRequest($cursor);
+
+ $response = $this->sendRequest($request);
+
+ return ListPromptsResult::fromArray($response->result);
+ }
+
+ /**
+ * Get a prompt from the server.
+ *
+ * @param string $name Prompt name
+ * @param array $arguments Prompt arguments
+ * @param (callable(float $progress, ?float $total, ?string $message): void)|null $onProgress
+ * Optional callback for progress updates
+ */
+ public function getPrompt(string $name, array $arguments = [], ?callable $onProgress = null): GetPromptResult
+ {
+ $request = new GetPromptRequest($name, $arguments);
+
+ $response = $this->sendRequest($request, $onProgress);
+
+ return GetPromptResult::fromArray($response->result);
+ }
+
+ /**
+ * Request completion suggestions for a prompt or resource argument.
+ *
+ * @param PromptReference|ResourceReference $ref The prompt or resource reference
+ * @param array{name: string, value: string} $argument The argument to complete
+ */
+ public function complete(PromptReference|ResourceReference $ref, array $argument): CompletionCompleteResult
+ {
+ $request = new CompletionCompleteRequest($ref, $argument);
+
+ $response = $this->sendRequest($request);
+
+ return CompletionCompleteResult::fromArray($response->result);
+ }
+
+ /**
+ * Set the minimum logging level for server log messages.
+ */
+ public function setLoggingLevel(LoggingLevel $level): void
+ {
+ $request = new SetLogLevelRequest($level);
+
+ $this->sendRequest($request);
+ }
+
+ /**
+ * Notify the server that the client's list of roots has changed.
+ *
+ * The server should react by requesting an updated list via roots/list.
+ *
+ * @throws RuntimeException if the client did not advertise the `roots.listChanged` capability
+ * @throws ConnectionException if the client is not connected
+ */
+ public function sendRootsListChanged(): void
+ {
+ if (true !== $this->config->capabilities->rootsListChanged) {
+ throw new RuntimeException('Cannot send a "roots/list_changed" notification without advertising the "roots.listChanged" capability. Build the client with new ClientCapabilities(roots: true, rootsListChanged: true).');
+ }
+
+ if (!$this->isConnected()) {
+ throw new ConnectionException('Client is not connected. Call connect() first.');
+ }
+
+ $this->protocol->sendNotification(new RootsListChangedNotification());
+ }
+
+ /**
+ * Send a request to the server and wait for response.
+ *
+ * @param (callable(float $progress, ?float $total, ?string $message): void)|null $onProgress
+ *
+ * @return Response
+ *
+ * @throws RequestException|ConnectionException
+ */
+ private function sendRequest(Request $request, ?callable $onProgress = null): Response
+ {
+ if (!$this->isConnected()) {
+ throw new ConnectionException('Client is not connected. Call connect() first.');
+ }
+
+ $withProgress = null !== $onProgress;
+ $fiber = new \Fiber(fn () => $this->protocol->request($request, $this->config->requestTimeout, $withProgress));
+ $response = $this->transport->runRequest($fiber, $onProgress);
+
+ if ($response instanceof Error) {
+ throw RequestException::fromError($response);
+ }
+
+ return $response;
+ }
+
+ /**
+ * Disconnect from the server.
+ */
+ public function disconnect(): void
+ {
+ if (null !== $this->transport) {
+ $this->transport->close();
+ $this->transport = null;
+ $this->logger->info('Client disconnected');
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Builder.php b/admin/vendor/mcp/sdk/src/Client/Builder.php
new file mode 100644
index 0000000..098b6e6
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Builder.php
@@ -0,0 +1,215 @@
+
+ */
+final class Builder
+{
+ private string $name = 'mcp-php-client';
+ private string $version = '1.0.0';
+ private ?string $description = null;
+ private ?string $title = null;
+ private ?ProtocolVersion $protocolVersion = null;
+ private ?ClientCapabilities $capabilities = null;
+
+ /** @var array> */
+ private array $extensions = [];
+ private int $initTimeout = 30;
+ private int $requestTimeout = 120;
+ private int $maxRetries = 3;
+ private ?LoggerInterface $logger = null;
+
+ /** @var NotificationHandlerInterface[] */
+ private array $notificationHandlers = [];
+
+ /** @var RequestHandlerInterface[] */
+ private array $requestHandlers = [];
+
+ /**
+ * Set the client name and version.
+ *
+ * @param ?string $title Display name for UI and end-user contexts. Falls back to $name when absent.
+ */
+ public function setClientInfo(string $name, string $version, ?string $description = null, ?string $title = null): self
+ {
+ $this->name = $name;
+ $this->version = $version;
+ $this->description = $description;
+ $this->title = $title;
+
+ return $this;
+ }
+
+ /**
+ * Set the protocol version to use.
+ */
+ public function setProtocolVersion(ProtocolVersion $protocolVersion): self
+ {
+ $this->protocolVersion = $protocolVersion;
+
+ return $this;
+ }
+
+ /**
+ * Set client capabilities.
+ */
+ public function setCapabilities(ClientCapabilities $capabilities): self
+ {
+ $this->capabilities = $capabilities;
+
+ return $this;
+ }
+
+ /**
+ * Enable one or more MCP protocol extensions, announced to the server under
+ * `capabilities.extensions` in the initialize request.
+ *
+ * @throws InvalidArgumentException if the identifier is not a valid `_meta` prefix
+ * @throws LogicException if the same extension is enabled more than once
+ */
+ public function enableExtension(ExtensionInterface ...$extensions): self
+ {
+ foreach ($extensions as $extension) {
+ $id = (string) $extension->getId();
+
+ if (isset($this->extensions[$id])) {
+ throw new LogicException(\sprintf('Extension "%s" is already enabled.', $id));
+ }
+
+ $this->extensions[$id] = $extension->getCapabilities();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set initialization timeout in seconds.
+ */
+ public function setInitTimeout(int $seconds): self
+ {
+ $this->initTimeout = $seconds;
+
+ return $this;
+ }
+
+ /**
+ * Set request timeout in seconds.
+ */
+ public function setRequestTimeout(int $seconds): self
+ {
+ $this->requestTimeout = $seconds;
+
+ return $this;
+ }
+
+ /**
+ * Set the number of times a failed connection attempt is retried.
+ *
+ * Counts retries, not attempts: 3 means one initial attempt plus up to three
+ * retries. Pass 0 to fail on the first failure. Only applies to
+ * {@see Client::connect()}; individual requests are never retried.
+ */
+ public function setMaxRetries(int $retries): self
+ {
+ $this->maxRetries = $retries;
+
+ return $this;
+ }
+
+ /**
+ * Set the logger.
+ */
+ public function setLogger(LoggerInterface $logger): self
+ {
+ $this->logger = $logger;
+
+ return $this;
+ }
+
+ /**
+ * Add a notification handler for server notifications.
+ */
+ public function addNotificationHandler(NotificationHandlerInterface $handler): self
+ {
+ $this->notificationHandlers[] = $handler;
+
+ return $this;
+ }
+
+ /**
+ * Add a request handler for server requests (e.g., sampling).
+ *
+ * @param RequestHandlerInterface $handler
+ */
+ public function addRequestHandler(RequestHandlerInterface $handler): self
+ {
+ $this->requestHandlers[] = $handler;
+
+ return $this;
+ }
+
+ /**
+ * Build the client instance.
+ */
+ public function build(): Client
+ {
+ $logger = $this->logger ?? new NullLogger();
+
+ $clientInfo = new Implementation(
+ $this->name,
+ $this->version,
+ $this->description,
+ title: $this->title,
+ );
+
+ $capabilities = $this->capabilities ?? new ClientCapabilities();
+
+ // Extensions enabled via enableExtension() are folded into caller-supplied
+ // capabilities too, so setCapabilities() does not silently drop them.
+ if ([] !== $this->extensions) {
+ $capabilities = $capabilities->withExtensions($this->extensions);
+ }
+
+ $config = new Configuration(
+ clientInfo: $clientInfo,
+ capabilities: $capabilities,
+ protocolVersion: $this->protocolVersion ?? ProtocolVersion::V2025_11_25,
+ initTimeout: $this->initTimeout,
+ requestTimeout: $this->requestTimeout,
+ maxRetries: $this->maxRetries,
+ );
+
+ $protocol = new Protocol(
+ requestHandlers: $this->requestHandlers,
+ notificationHandlers: $this->notificationHandlers,
+ logger: $logger,
+ );
+
+ return new Client($protocol, $config, $logger);
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Configuration.php b/admin/vendor/mcp/sdk/src/Client/Configuration.php
new file mode 100644
index 0000000..f0ed6f7
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Configuration.php
@@ -0,0 +1,46 @@
+
+ */
+class Configuration
+{
+ public function __construct(
+ public readonly Implementation $clientInfo,
+ public readonly ClientCapabilities $capabilities,
+ public readonly ProtocolVersion $protocolVersion = ProtocolVersion::V2025_11_25,
+ public readonly int $initTimeout = 30,
+ public readonly int $requestTimeout = 120,
+ public readonly int $maxRetries = 3,
+ ) {
+ if ($initTimeout < 1) {
+ throw new InvalidArgumentException(\sprintf('The initialization timeout must be a positive number of seconds, got %d.', $initTimeout));
+ }
+
+ if ($requestTimeout < 1) {
+ throw new InvalidArgumentException(\sprintf('The request timeout must be a positive number of seconds, got %d.', $requestTimeout));
+ }
+
+ if ($maxRetries < 0) {
+ throw new InvalidArgumentException(\sprintf('The maximum number of retries must be zero or greater, got %d.', $maxRetries));
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Handler/Notification/LoggingNotificationHandler.php b/admin/vendor/mcp/sdk/src/Client/Handler/Notification/LoggingNotificationHandler.php
new file mode 100644
index 0000000..a6853d4
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Handler/Notification/LoggingNotificationHandler.php
@@ -0,0 +1,47 @@
+
+ *
+ * @deprecated since protocol revision 2026-07-28 (SEP-2577), earliest removal 2027-07-28.
+ * Log to stderr (stdio) or use OpenTelemetry instead.
+ */
+class LoggingNotificationHandler implements NotificationHandlerInterface
+{
+ /**
+ * @param callable(LoggingMessageNotification): void $callback
+ */
+ public function __construct(
+ private readonly mixed $callback,
+ ) {
+ trigger_deprecation('mcp/sdk', '0.8', 'MCP logging is deprecated since protocol revision 2026-07-28 (SEP-2577); log to stderr or use OpenTelemetry instead.');
+ }
+
+ public function supports(Notification $notification): bool
+ {
+ return $notification instanceof LoggingMessageNotification;
+ }
+
+ public function handle(Notification $notification): void
+ {
+ \assert($notification instanceof LoggingMessageNotification);
+
+ ($this->callback)($notification);
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Handler/Notification/NotificationHandlerInterface.php b/admin/vendor/mcp/sdk/src/Client/Handler/Notification/NotificationHandlerInterface.php
new file mode 100644
index 0000000..82092aa
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Handler/Notification/NotificationHandlerInterface.php
@@ -0,0 +1,32 @@
+
+ */
+interface NotificationHandlerInterface
+{
+ /**
+ * Check if this handler supports the given notification.
+ */
+ public function supports(Notification $notification): bool;
+
+ /**
+ * Handle the notification.
+ */
+ public function handle(Notification $notification): void;
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Handler/Notification/ProgressNotificationHandler.php b/admin/vendor/mcp/sdk/src/Client/Handler/Notification/ProgressNotificationHandler.php
new file mode 100644
index 0000000..3c489bf
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Handler/Notification/ProgressNotificationHandler.php
@@ -0,0 +1,52 @@
+
+ *
+ * @internal
+ */
+class ProgressNotificationHandler implements NotificationHandlerInterface
+{
+ public function __construct(
+ private readonly ClientStateInterface $state,
+ ) {
+ }
+
+ public function supports(Notification $notification): bool
+ {
+ return $notification instanceof ProgressNotification;
+ }
+
+ public function handle(Notification $notification): void
+ {
+ if (!$notification instanceof ProgressNotification) {
+ return;
+ }
+
+ $this->state->storeProgress(
+ (string) $notification->progressToken,
+ $notification->progress,
+ $notification->total,
+ $notification->message,
+ );
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Handler/Request/ElicitationCallbackInterface.php b/admin/vendor/mcp/sdk/src/Client/Handler/Request/ElicitationCallbackInterface.php
new file mode 100644
index 0000000..fdaea14
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Handler/Request/ElicitationCallbackInterface.php
@@ -0,0 +1,26 @@
+
+ *
+ * @author Johannes Wachter
+ */
+class ElicitationRequestHandler implements RequestHandlerInterface
+{
+ public function __construct(
+ private readonly ElicitationCallbackInterface $callback,
+ private readonly LoggerInterface $logger = new NullLogger(),
+ ) {
+ }
+
+ public function supports(Request $request): bool
+ {
+ return $request instanceof ElicitRequest;
+ }
+
+ /**
+ * @return Response|Error
+ */
+ public function handle(Request $request): Response|Error
+ {
+ \assert($request instanceof ElicitRequest);
+
+ try {
+ $result = $this->callback->__invoke($request);
+
+ return new Response($request->getId(), $result);
+ } catch (ElicitationException $e) {
+ $this->logger->error('Elicitation failed: '.$e->getMessage(), ['exception' => $e]);
+
+ return Error::forInternalError($e->getMessage(), $request->getId());
+ } catch (\Throwable $e) {
+ $this->logger->error('Unexpected error during elicitation', ['exception' => $e]);
+
+ return Error::forInternalError('Error while processing elicitation', $request->getId());
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Handler/Request/ListRootsRequestHandler.php b/admin/vendor/mcp/sdk/src/Client/Handler/Request/ListRootsRequestHandler.php
new file mode 100644
index 0000000..50806c1
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Handler/Request/ListRootsRequestHandler.php
@@ -0,0 +1,73 @@
+
+ *
+ * @author Johannes Wachter
+ *
+ * @deprecated since protocol revision 2026-07-28 (SEP-2577), earliest removal 2027-07-28.
+ * Pass directories or files through tool arguments, resource
+ * URIs or server configuration instead.
+ */
+class ListRootsRequestHandler implements RequestHandlerInterface
+{
+ public function __construct(
+ private readonly RootsCallbackInterface $callback,
+ private readonly LoggerInterface $logger = new NullLogger(),
+ ) {
+ trigger_deprecation('mcp/sdk', '0.8', 'MCP roots are deprecated since protocol revision 2026-07-28 (SEP-2577); pass directories through tool arguments or resource URIs instead.');
+ }
+
+ public function supports(Request $request): bool
+ {
+ return $request instanceof ListRootsRequest;
+ }
+
+ /**
+ * @return Response|Error
+ */
+ public function handle(Request $request): Response|Error
+ {
+ \assert($request instanceof ListRootsRequest);
+
+ try {
+ $result = $this->callback->__invoke($request);
+
+ return new Response($request->getId(), $result);
+ } catch (RootsException $e) {
+ $this->logger->error('Listing roots failed: '.$e->getMessage(), ['exception' => $e]);
+
+ return Error::forInternalError($e->getMessage(), $request->getId());
+ } catch (\Throwable $e) {
+ $this->logger->error('Unexpected error while listing roots', ['exception' => $e]);
+
+ return Error::forInternalError('Error while listing roots', $request->getId());
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Handler/Request/RequestHandlerInterface.php b/admin/vendor/mcp/sdk/src/Client/Handler/Request/RequestHandlerInterface.php
new file mode 100644
index 0000000..1c05018
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Handler/Request/RequestHandlerInterface.php
@@ -0,0 +1,38 @@
+
+ */
+interface RequestHandlerInterface
+{
+ /**
+ * Check if this handler supports the given request.
+ */
+ public function supports(Request $request): bool;
+
+ /**
+ * Handle the request and return a response or error.
+ *
+ * @return Response|Error
+ */
+ public function handle(Request $request): Response|Error;
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Handler/Request/RootsCallbackInterface.php b/admin/vendor/mcp/sdk/src/Client/Handler/Request/RootsCallbackInterface.php
new file mode 100644
index 0000000..9dcccb2
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Handler/Request/RootsCallbackInterface.php
@@ -0,0 +1,32 @@
+
+ *
+ * @deprecated since protocol revision 2026-07-28 (SEP-2577), earliest removal 2027-07-28.
+ * Pass directories or files through tool arguments, resource
+ * URIs or server configuration instead.
+ */
+interface RootsCallbackInterface
+{
+ public function __invoke(ListRootsRequest $request): ListRootsResult;
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Handler/Request/SamplingCallbackInterface.php b/admin/vendor/mcp/sdk/src/Client/Handler/Request/SamplingCallbackInterface.php
new file mode 100644
index 0000000..0761de3
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Handler/Request/SamplingCallbackInterface.php
@@ -0,0 +1,28 @@
+
+ *
+ * @author Kyrian Obikwelu
+ *
+ * @deprecated since protocol revision 2026-07-28 (SEP-2577), earliest removal 2027-07-28.
+ * Integrate with an LLM provider's API directly instead.
+ */
+class SamplingRequestHandler implements RequestHandlerInterface
+{
+ public function __construct(
+ private readonly SamplingCallbackInterface $callback,
+ private readonly LoggerInterface $logger = new NullLogger(),
+ ) {
+ trigger_deprecation('mcp/sdk', '0.8', 'MCP sampling is deprecated since protocol revision 2026-07-28 (SEP-2577); integrate with an LLM provider\'s API directly instead.');
+ }
+
+ public function supports(Request $request): bool
+ {
+ return $request instanceof CreateSamplingMessageRequest;
+ }
+
+ /**
+ * @return Response|Error
+ */
+ public function handle(Request $request): Response|Error
+ {
+ \assert($request instanceof CreateSamplingMessageRequest);
+
+ try {
+ $request->validateToolFlow();
+ } catch (InvalidArgumentException $e) {
+ $this->logger->warning('Rejecting sampling request violating the tool flow', ['exception' => $e]);
+
+ return Error::forInvalidParams($e->getMessage(), $request->getId());
+ }
+
+ try {
+ $result = $this->callback->__invoke($request);
+
+ return new Response($request->getId(), $result);
+ } catch (SamplingException $e) {
+ $this->logger->error('Sampling failed: '.$e->getMessage(), ['exception' => $e]);
+
+ return Error::forInternalError($e->getMessage(), $request->getId());
+ } catch (\Throwable $e) {
+ $this->logger->error('Unexpected error during sampling', ['exception' => $e]);
+
+ return Error::forInternalError('Error while sampling LLM', $request->getId());
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Protocol.php b/admin/vendor/mcp/sdk/src/Client/Protocol.php
new file mode 100644
index 0000000..281aedc
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Protocol.php
@@ -0,0 +1,689 @@
+
+ */
+class Protocol
+{
+ /**
+ * How many times a request may be re-sent before the client gives up.
+ *
+ * Both loops that re-send are bounded by it: a server that keeps asking for
+ * input, and one that keeps rejecting the offered revision. Neither is
+ * expected to run more than a round or two, so the cap is only there to
+ * stop a broken or hostile server from spinning the client forever.
+ */
+ private const MAX_ROUND_TRIPS = 10;
+
+ private ?TransportInterface $transport = null;
+ private ClientStateInterface $state;
+ private MessageFactory $messageFactory;
+ private LoggerInterface $logger;
+
+ /** @var NotificationHandlerInterface[] */
+ private array $notificationHandlers;
+
+ /** Set only when the configured revision has no handshake. */
+ private ?RequestEnvelope $envelope = null;
+
+ private ?HeaderFactory $headers = null;
+
+ private ToolCatalog $tools;
+
+ private readonly InputRequestResolver $inputRequests;
+
+ /**
+ * Progress tokens are only required to be unique within a connection, and
+ * a retry keeps the caller's one — the work being reported on is the same.
+ */
+ private int $progressTokens = 0;
+
+ /**
+ * @param RequestHandlerInterface[] $requestHandlers
+ * @param NotificationHandlerInterface[] $notificationHandlers
+ */
+ public function __construct(
+ private readonly array $requestHandlers = [],
+ array $notificationHandlers = [],
+ ?MessageFactory $messageFactory = null,
+ ?LoggerInterface $logger = null,
+ ) {
+ $this->state = new ClientState();
+ $this->messageFactory = $messageFactory ?? MessageFactory::make();
+ $this->logger = $logger ?? new NullLogger();
+
+ $this->notificationHandlers = [
+ new ProgressNotificationHandler($this->state),
+ ...$notificationHandlers,
+ ];
+
+ $this->tools = new ToolCatalog($this->logger);
+ $this->inputRequests = new InputRequestResolver($this->requestHandlers, $this->logger);
+ }
+
+ /**
+ * What the client knows about the server's tools, from `tools/list`.
+ *
+ * Kept on the protocol rather than the facade because it is what makes the
+ * SEP-2243 headers derivable at send time.
+ */
+ public function getToolCatalog(): ToolCatalog
+ {
+ return $this->tools;
+ }
+
+ /**
+ * Connect this protocol to a transport.
+ *
+ * Sets up message handling callbacks.
+ *
+ * @param TransportInterface $transport The transport to connect
+ * @param Configuration $config The client configuration for initialization
+ */
+ public function connect(TransportInterface $transport, Configuration $config): void
+ {
+ $this->transport = $transport;
+
+ // A fresh catalog per connection: it is what a server told this client
+ // about its tools, and a server reached by reconnecting — the same one
+ // or another — has said nothing yet.
+ $this->tools = new ToolCatalog($this->logger);
+
+ if ($config->protocolVersion->isModern()) {
+ $this->envelope = new RequestEnvelope(
+ $config->protocolVersion,
+ $config->capabilities,
+ $config->clientInfo,
+ );
+ $this->headers = new HeaderFactory($this->tools);
+ }
+
+ $transport->setState($this->state);
+ $transport->onInitialize(fn () => $this->initialize($config));
+ $transport->onMessage($this->processMessage(...));
+ $transport->onError(fn (\Throwable $e) => $this->logger->error('Transport error', ['exception' => $e]));
+
+ if ($transport instanceof HeaderAwareTransportInterface) {
+ $transport->onHeaders($this->headersFor(...));
+ }
+
+ $this->logger->info('Protocol connected to transport', ['transport' => $transport::class]);
+ }
+
+ /**
+ * The headers belonging to an encoded message, for a transport that has any.
+ *
+ * @return array
+ */
+ private function headersFor(string $payload): array
+ {
+ if (null === $this->headers || null === $this->envelope) {
+ return [];
+ }
+
+ $decoded = json_decode($payload, true);
+
+ return \is_array($decoded)
+ ? $this->headers->forMessage($decoded, $this->envelope->protocolVersion())
+ : [];
+ }
+
+ /**
+ * Ready the connection for use.
+ *
+ * Up to 2025-11-25 that means the `initialize` handshake: offer a revision,
+ * take the server's answer, confirm with `notifications/initialized`. From
+ * 2026-07-28 there is no handshake at all — see {@see self::discover()}.
+ *
+ * @param Configuration $config The client configuration
+ *
+ * @return Response>|Error
+ */
+ public function initialize(Configuration $config): Response|Error
+ {
+ if (null !== $this->envelope) {
+ return $this->discover($config);
+ }
+
+ $offered = $config->protocolVersion;
+
+ $request = new InitializeRequest(
+ $offered->value,
+ $config->capabilities,
+ $config->clientInfo,
+ );
+
+ $response = $this->request($request, $config->initTimeout);
+
+ if ($response instanceof Response) {
+ $initResult = InitializeResult::fromArray($response->result);
+
+ // A counter-offer this SDK cannot speak leaves nothing to fall back to,
+ // so the handshake fails rather than continuing on a revision neither
+ // side agrees on.
+ $negotiated = $initResult->protocolVersion;
+ if (null === $negotiated || $negotiated->isModern()) {
+ // fromArray() above already rejected a missing or non-string revision.
+ $counterOffer = (string) $response->result['protocolVersion'];
+
+ return Error::forInvalidParams(\sprintf(
+ 'Server responded with unsupported protocol version "%s". Supported versions: %s.',
+ $counterOffer,
+ implode(', ', array_map(
+ static fn (ProtocolVersion $v): string => $v->value,
+ ProtocolVersion::handshakeVersions(),
+ )),
+ ), $response->id);
+ }
+
+ $this->state->setProtocolVersion($negotiated);
+ $this->state->setServerInfo($initResult->serverInfo);
+ $this->state->setInstructions($initResult->instructions);
+ $this->state->setInitialized(true);
+
+ $this->sendNotification(new InitializedNotification());
+
+ $this->logger->info('Initialization complete', [
+ 'server' => $initResult->serverInfo->name,
+ 'protocolVersion' => $negotiated->value,
+ ]);
+ }
+
+ return $response;
+ }
+
+ /**
+ * Stand in for the handshake in the modern era.
+ *
+ * There is nothing to negotiate: the revision travels on every request, so
+ * the connection is usable the moment the transport is. `server/discover`
+ * is only asked because the facade exposes `getServerInfo()`, and a server
+ * that will not answer it still serves every other method — so a failure
+ * here is logged and the connection proceeds.
+ *
+ * @return Response>
+ */
+ private function discover(Configuration $config): Response
+ {
+ $this->state->setProtocolVersion($config->protocolVersion);
+ $this->state->setInitialized(true);
+
+ $response = $this->request(new DiscoverRequest(), $config->initTimeout);
+
+ if ($response instanceof Error) {
+ $this->logger->info('Server did not answer "server/discover"; continuing without its metadata.', [
+ 'code' => $response->code,
+ 'message' => $response->message,
+ ]);
+
+ return new Response(0, []);
+ }
+
+ $this->readDiscovery($response->result);
+
+ return $response;
+ }
+
+ /**
+ * Read defensively: `server/discover` is optional, so a server may answer
+ * with something that is not a DiscoverResult at all, and none of it is
+ * load-bearing for the requests that follow.
+ *
+ * @param array $result
+ */
+ private function readDiscovery(array $result): void
+ {
+ // Identity is wire vocabulary in this revision, so it rides in `_meta`
+ // rather than the result body. The top level is read as a fallback
+ // because that is where the handshake era put it.
+ $meta = \is_array($result['_meta'] ?? null) ? $result['_meta'] : [];
+ $serverInfo = $meta[RequestMeta::SERVER_INFO] ?? $result['serverInfo'] ?? null;
+
+ if (\is_array($serverInfo)) {
+ try {
+ $this->state->setServerInfo(Implementation::fromArray($serverInfo));
+ } catch (\Throwable $e) {
+ $this->logger->debug('Ignoring unreadable serverInfo from "server/discover".', ['exception' => $e]);
+ }
+ }
+
+ if (\is_string($result['instructions'] ?? null)) {
+ $this->state->setInstructions($result['instructions']);
+ }
+
+ $this->reconcileVersion($result['supportedVersions'] ?? null);
+
+ $this->logger->info('Discovery complete', [
+ 'supportedVersions' => $result['supportedVersions'] ?? null,
+ ]);
+ }
+
+ /**
+ * Move to a revision the server actually speaks, if it said which.
+ *
+ * `server/discover` reports rather than negotiates, so a client that asked
+ * for something the server does not list learns it here — and learning it
+ * now is far better than a stream of refusals later. A server that stays
+ * silent about its versions is left alone; the method is optional and
+ * saying nothing is not the same as saying no.
+ */
+ private function reconcileVersion(mixed $supportedVersions): void
+ {
+ if (!\is_array($supportedVersions) || [] === $supportedVersions || null === $this->envelope) {
+ return;
+ }
+
+ $current = $this->envelope->protocolVersion();
+
+ if (\in_array($current->value, $supportedVersions, true)) {
+ return;
+ }
+
+ foreach ($supportedVersions as $candidate) {
+ $version = \is_string($candidate) ? ProtocolVersion::tryFrom($candidate) : null;
+
+ if (null === $version || !$version->isModern()) {
+ continue;
+ }
+
+ $this->logger->warning('Server does not speak the configured revision; continuing on one it advertises.', [
+ 'configured' => $current->value,
+ 'using' => $version->value,
+ ]);
+
+ $this->envelope = $this->envelope->withProtocolVersion($version);
+ $this->state->setProtocolVersion($version);
+
+ return;
+ }
+
+ // Everything it offers is handshake era, which this connection cannot
+ // reach — it has already skipped the handshake.
+ throw new ConnectionException(\sprintf('Server does not support any modern protocol revision (it advertises %s); the configured "%s" cannot be used against it.', implode(', ', array_map(strval(...), $supportedVersions)), $current->value));
+ }
+
+ /**
+ * Send a request to the server and wait for response.
+ *
+ * If a response is immediately available (sync HTTP), returns it.
+ * Otherwise, suspends the Fiber and waits for the transport to resume it.
+ *
+ * In the modern era this is also where the two loops that re-send live:
+ * answering a server's request for input (SEP-2322), and retrying under a
+ * revision the server accepts (SEP-2575). Both re-send the same call, so
+ * they belong together and above the single exchange.
+ *
+ * @param Request $request The request to send
+ * @param int $timeout The timeout in seconds
+ * @param bool $withProgress Whether to attach a progress token to the request
+ *
+ * @return Response>|Error
+ */
+ public function request(Request $request, int $timeout, bool $withProgress = false): Response|Error
+ {
+ $payload = $request->withId(0)->jsonSerialize();
+ unset($payload['id']);
+
+ if ($withProgress) {
+ $payload = self::withMeta($payload, ['progressToken' => 'prog-'.++$this->progressTokens]);
+ }
+
+ if (null === $this->envelope) {
+ return $this->exchange($payload, $timeout);
+ }
+
+ for ($attempt = 0; $attempt < self::MAX_ROUND_TRIPS; ++$attempt) {
+ $response = $this->exchange($payload, $timeout);
+
+ if ($response instanceof Error) {
+ $retry = $this->withAcceptedVersion($response);
+
+ if (null === $retry) {
+ return $response;
+ }
+
+ continue;
+ }
+
+ $asked = InputRequestResolver::asked($response->result);
+
+ if (null === $asked) {
+ return $response;
+ }
+
+ // A fresh `inputResponses`/`requestState` pair every round, never
+ // merged with the last: the answers belong to the ask that just
+ // arrived, and carrying an old one forward is how state leaks
+ // between rounds.
+ //
+ // Cast to object: inputResponses is a JSON object keyed by the
+ // server's ids, but a PHP array with no entries or with sequential
+ // numeric-string keys encodes as a JSON array instead.
+ $payload['params'] = [
+ ...($payload['params'] ?? []),
+ 'inputResponses' => (object) $this->inputRequests->resolve($asked),
+ ];
+
+ unset($payload['params']['requestState']);
+
+ // Echoed byte-for-byte, and only when the server sent one: the
+ // value is the server's to read, and inventing or reshaping it
+ // would break whatever it encodes.
+ if (\is_string($response->result['requestState'] ?? null)) {
+ $payload['params']['requestState'] = $response->result['requestState'];
+ }
+
+ $this->logger->debug('Retrying request with resolved input', [
+ 'method' => $payload['method'] ?? null,
+ 'round' => $attempt + 1,
+ ]);
+ }
+
+ return Error::forInternalError(\sprintf('Server asked for input more than %d times without completing the request.', self::MAX_ROUND_TRIPS));
+ }
+
+ /**
+ * Switches the offered revision when the server refuses the current one,
+ * or null when there is nothing to retry with.
+ *
+ * @param Error $error the server's refusal
+ */
+ private function withAcceptedVersion(Error $error): ?ProtocolVersion
+ {
+ if (Error::UNSUPPORTED_PROTOCOL_VERSION !== $error->code || null === $this->envelope) {
+ return null;
+ }
+
+ $data = \is_array($error->data) ? $error->data : [];
+ $supported = \is_array($data['supported'] ?? null) ? $data['supported'] : [];
+ $current = $this->envelope->protocolVersion();
+
+ foreach ($supported as $candidate) {
+ $version = \is_string($candidate) ? ProtocolVersion::tryFrom($candidate) : null;
+
+ // Only another modern revision is reachable from here: falling back
+ // to a handshake era one would mean opening a connection this
+ // transport already decided it was not going to open.
+ if (null === $version || !$version->isModern() || $version === $current) {
+ continue;
+ }
+
+ $this->logger->info('Server rejected the offered protocol revision, retrying with one it supports.', [
+ 'offered' => $current->value,
+ 'retrying' => $version->value,
+ ]);
+
+ $this->envelope = $this->envelope->withProtocolVersion($version);
+ $this->state->setProtocolVersion($version);
+
+ return $version;
+ }
+
+ return null;
+ }
+
+ /**
+ * One request on the wire: assign an id, send, and wait for its answer.
+ *
+ * A retry gets a new id, because the previous one is spent — the server has
+ * already answered it, and reusing it would make the two indistinguishable.
+ *
+ * @param array $payload
+ *
+ * @return Response>|Error
+ */
+ private function exchange(array $payload, int $timeout): Response|Error
+ {
+ $requestId = $this->state->nextRequestId();
+ $payload['id'] = $requestId;
+
+ $this->state->addPendingRequest($requestId, $timeout);
+
+ try {
+ $this->send($payload, 'request');
+
+ $immediate = $this->state->consumeResponse($requestId);
+ if (null !== $immediate) {
+ $this->logger->debug('Received immediate response', ['id' => $requestId]);
+
+ return $immediate;
+ }
+
+ $this->logger->debug('Suspending fiber for response', ['id' => $requestId]);
+
+ return \Fiber::suspend([
+ 'type' => 'await_response',
+ 'request_id' => $requestId,
+ 'timeout' => $timeout,
+ ]);
+ } finally {
+ // Only the response path clears it, so a request that timed out or
+ // whose send() threw would stay pending and fail every later one.
+ $this->state->removePendingRequest($requestId);
+ }
+ }
+
+ /**
+ * Send a notification to the server (fire and forget).
+ */
+ public function sendNotification(Notification $notification): void
+ {
+ $this->send($notification->jsonSerialize(), 'notification');
+ }
+
+ /**
+ * Encode and hand a message to the transport, stamping the per-request
+ * envelope on the way out when the revision calls for one.
+ *
+ * @param array $payload
+ */
+ private function send(array $payload, string $kind): void
+ {
+ if (null !== $this->envelope) {
+ $payload = $this->envelope->stamp($payload);
+ }
+
+ $this->logger->debug('Sending '.$kind, [
+ 'id' => $payload['id'] ?? null,
+ 'method' => $payload['method'] ?? null,
+ ]);
+
+ $this->transport?->send(json_encode($payload, \JSON_THROW_ON_ERROR));
+ }
+
+ /**
+ * @param array $payload
+ * @param array $meta
+ *
+ * @return array
+ */
+ private static function withMeta(array $payload, array $meta): array
+ {
+ $params = \is_array($payload['params'] ?? null) ? $payload['params'] : [];
+ $existing = \is_array($params['_meta'] ?? null) ? $params['_meta'] : [];
+
+ $params['_meta'] = [...$existing, ...$meta];
+ $payload['params'] = $params;
+
+ return $payload;
+ }
+
+ /**
+ * Send a response back to the server (for server-initiated requests).
+ *
+ * @param Response|Error $response
+ */
+ private function sendResponse(Response|Error $response): void
+ {
+ $this->logger->debug('Sending response', ['id' => $response->getId()]);
+
+ $encoded = json_encode($response, \JSON_THROW_ON_ERROR);
+ $this->transport?->send($encoded);
+ }
+
+ /**
+ * Process an incoming message from the server.
+ *
+ * Routes to appropriate handler based on message type.
+ */
+ public function processMessage(string $input): void
+ {
+ $this->logger->debug('Received message', ['input' => $input]);
+
+ try {
+ $messages = $this->messageFactory->create($input);
+ } catch (\JsonException $e) {
+ $this->logger->warning('Failed to parse message', ['exception' => $e]);
+
+ return;
+ }
+
+ foreach ($messages as $message) {
+ if ($message instanceof Response || $message instanceof Error) {
+ $this->handleResponse($message);
+ } elseif ($message instanceof Request) {
+ $this->handleRequest($message);
+ } elseif ($message instanceof Notification) {
+ $this->handleNotification($message);
+ }
+ }
+ }
+
+ /**
+ * Handle a response from the server.
+ *
+ * This stores it in session. The transport will pick it up and resume the Fiber.
+ *
+ * @param Response|Error $response
+ */
+ private function handleResponse(Response|Error $response): void
+ {
+ $requestId = $response->getId();
+
+ if (null === $requestId) {
+ $this->logger->warning('Received an id-less error response; cannot correlate it to a request.', ['response' => $response->jsonSerialize()]);
+
+ return;
+ }
+
+ $this->logger->debug('Handling response', ['id' => $requestId]);
+
+ $this->state->storeResponse($requestId, $response->jsonSerialize());
+ }
+
+ /**
+ * Handle a request from the server (e.g., sampling request).
+ */
+ private function handleRequest(Request $request): void
+ {
+ $method = $request::getMethod();
+
+ $this->logger->debug('Received server request', [
+ 'method' => $method,
+ 'id' => $request->getId(),
+ ]);
+
+ foreach ($this->requestHandlers as $handler) {
+ if ($handler->supports($request)) {
+ try {
+ $response = $handler->handle($request);
+ } catch (\Throwable $e) {
+ $this->logger->error('Unexpected error while handling request', [
+ 'method' => $method,
+ 'exception' => $e,
+ ]);
+
+ $response = Error::forInternalError(
+ \sprintf('Unexpected error while handling "%s" request', $method),
+ $request->getId()
+ );
+ }
+
+ $this->sendResponse($response);
+
+ return;
+ }
+ }
+
+ $error = Error::forMethodNotFound(
+ \sprintf('Client does not handle "%s" requests.', $method),
+ $request->getId()
+ );
+
+ $this->sendResponse($error);
+ }
+
+ /**
+ * Handle a notification from the server.
+ */
+ private function handleNotification(Notification $notification): void
+ {
+ $method = $notification::getMethod();
+
+ $this->logger->debug('Received server notification', [
+ 'method' => $method,
+ ]);
+
+ foreach ($this->notificationHandlers as $handler) {
+ if ($handler->supports($notification)) {
+ try {
+ $handler->handle($notification);
+ } catch (\Throwable $e) {
+ $this->logger->warning('Notification handler failed', ['exception' => $e]);
+ }
+
+ return;
+ }
+ }
+ }
+
+ public function getState(): ClientStateInterface
+ {
+ return $this->state;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/State/ClientState.php b/admin/vendor/mcp/sdk/src/Client/State/ClientState.php
new file mode 100644
index 0000000..a1e1f89
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/State/ClientState.php
@@ -0,0 +1,148 @@
+
+ */
+class ClientState implements ClientStateInterface
+{
+ private int $requestIdCounter = 1;
+ private bool $initialized = false;
+ private ?ProtocolVersion $protocolVersion = null;
+ private ?Implementation $serverInfo = null;
+ private ?string $instructions = null;
+
+ /** @var array */
+ private array $pendingRequests = [];
+
+ /** @var array> */
+ private array $responses = [];
+
+ /** @var array */
+ private array $progressUpdates = [];
+
+ public function nextRequestId(): int
+ {
+ return $this->requestIdCounter++;
+ }
+
+ public function addPendingRequest(int|string $requestId, int $timeout): void
+ {
+ $this->pendingRequests[$requestId] = [
+ 'request_id' => $requestId,
+ 'timestamp' => time(),
+ 'timeout' => $timeout,
+ ];
+ }
+
+ public function removePendingRequest(int|string $requestId): void
+ {
+ unset($this->pendingRequests[$requestId]);
+ }
+
+ public function getPendingRequests(): array
+ {
+ return $this->pendingRequests;
+ }
+
+ public function storeResponse(int|string $requestId, array $responseData): void
+ {
+ $this->responses[$requestId] = $responseData;
+ }
+
+ public function consumeResponse(int|string $requestId): Response|Error|null
+ {
+ if (!isset($this->responses[$requestId])) {
+ return null;
+ }
+
+ $data = $this->responses[$requestId];
+ unset($this->responses[$requestId]);
+ $this->removePendingRequest($requestId);
+
+ if (isset($data['error'])) {
+ return Error::fromArray($data);
+ }
+
+ return Response::fromArray($data);
+ }
+
+ public function setInitialized(bool $initialized): void
+ {
+ $this->initialized = $initialized;
+ }
+
+ public function isInitialized(): bool
+ {
+ return $this->initialized;
+ }
+
+ public function setProtocolVersion(ProtocolVersion $protocolVersion): void
+ {
+ $this->protocolVersion = $protocolVersion;
+ }
+
+ public function getProtocolVersion(): ?ProtocolVersion
+ {
+ return $this->protocolVersion;
+ }
+
+ public function setServerInfo(Implementation $serverInfo): void
+ {
+ $this->serverInfo = $serverInfo;
+ }
+
+ public function getServerInfo(): ?Implementation
+ {
+ return $this->serverInfo;
+ }
+
+ public function setInstructions(?string $instructions): void
+ {
+ $this->instructions = $instructions;
+ }
+
+ public function getInstructions(): ?string
+ {
+ return $this->instructions;
+ }
+
+ public function storeProgress(string $token, float $progress, ?float $total, ?string $message): void
+ {
+ $this->progressUpdates[] = [
+ 'token' => $token,
+ 'progress' => $progress,
+ 'total' => $total,
+ 'message' => $message,
+ ];
+ }
+
+ public function consumeProgressUpdates(): array
+ {
+ $updates = $this->progressUpdates;
+ $this->progressUpdates = [];
+
+ return $updates;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/State/ClientStateInterface.php b/admin/vendor/mcp/sdk/src/Client/State/ClientStateInterface.php
new file mode 100644
index 0000000..a5de42d
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/State/ClientStateInterface.php
@@ -0,0 +1,128 @@
+
+ */
+interface ClientStateInterface
+{
+ /**
+ * Get the next request ID for outgoing requests.
+ */
+ public function nextRequestId(): int;
+
+ /**
+ * Add a pending request to track.
+ *
+ * @param int|string $requestId The request ID
+ * @param int $timeout Timeout in seconds
+ */
+ public function addPendingRequest(int|string $requestId, int $timeout): void;
+
+ /**
+ * Remove a pending request.
+ */
+ public function removePendingRequest(int|string $requestId): void;
+
+ /**
+ * Get all pending requests.
+ *
+ * @return array
+ */
+ public function getPendingRequests(): array;
+
+ /**
+ * Store a received response.
+ *
+ * @param int|string $requestId The request ID
+ * @param array $responseData The raw response data
+ */
+ public function storeResponse(int|string $requestId, array $responseData): void;
+
+ /**
+ * Check and consume a response for a request ID.
+ *
+ * @return Response>|Error|null
+ */
+ public function consumeResponse(int|string $requestId): Response|Error|null;
+
+ /**
+ * Set initialization state.
+ */
+ public function setInitialized(bool $initialized): void;
+
+ /**
+ * Check if connection is initialized.
+ */
+ public function isInitialized(): bool;
+
+ /**
+ * Store the protocol version negotiated during initialization.
+ */
+ public function setProtocolVersion(ProtocolVersion $protocolVersion): void;
+
+ /**
+ * Get the protocol version negotiated during initialization.
+ *
+ * Null until the handshake has completed.
+ */
+ public function getProtocolVersion(): ?ProtocolVersion;
+
+ /**
+ * Store the server info from initialization.
+ */
+ public function setServerInfo(Implementation $serverInfo): void;
+
+ /**
+ * Get the server info from initialization.
+ */
+ public function getServerInfo(): ?Implementation;
+
+ /**
+ * Store the server instructions from initialization.
+ */
+ public function setInstructions(?string $instructions): void;
+
+ /**
+ * Get the server instructions from initialization.
+ */
+ public function getInstructions(): ?string;
+
+ /**
+ * Store progress data received from a notification.
+ *
+ * @param string $token The progress token
+ * @param float $progress Current progress value
+ * @param float|null $total Total progress value (if known)
+ * @param string|null $message Progress message
+ */
+ public function storeProgress(string $token, float $progress, ?float $total, ?string $message): void;
+
+ /**
+ * Consume all pending progress updates.
+ *
+ * @return array
+ */
+ public function consumeProgressUpdates(): array;
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Stateless/HeaderFactory.php b/admin/vendor/mcp/sdk/src/Client/Stateless/HeaderFactory.php
new file mode 100644
index 0000000..428e1c7
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Stateless/HeaderFactory.php
@@ -0,0 +1,102 @@
+
+ */
+final class HeaderFactory
+{
+ public function __construct(
+ private readonly ToolCatalog $tools,
+ ) {
+ }
+
+ /**
+ * @param array $payload a serialized JSON-RPC message
+ *
+ * @return array
+ */
+ public function forMessage(array $payload, ProtocolVersion $protocolVersion): array
+ {
+ $method = $payload['method'] ?? null;
+ $params = \is_array($payload['params'] ?? null) ? $payload['params'] : null;
+
+ // A response to a server-initiated request carries no method to mirror;
+ // the version header is unconditional and still applies. Trace context
+ // still rides along if the application put it in `_meta` — the party
+ // that started the trace is not necessarily the one with a method to
+ // report.
+ if (!\is_string($method)) {
+ return [McpHeader::PROTOCOL_VERSION => $protocolVersion->value, ...$this->traceHeaders($params)];
+ }
+
+ $headers = [
+ McpHeader::PROTOCOL_VERSION => $protocolVersion->value,
+ McpHeader::METHOD => $method,
+ ...$this->traceHeaders($params),
+ ];
+
+ if (null !== $name = McpHeader::nameFor($method, $params)) {
+ // Tool and prompt names are only SHOULD-constrained to header-safe
+ // characters and a resource URI is not constrained at all, so
+ // anything unsafe is wrapped rather than dropped or mangled.
+ $headers[McpHeader::NAME] = McpHeader::encode($name) ?? $name;
+ }
+
+ if ('tools/call' === $method && \is_string($params['name'] ?? null)) {
+ $arguments = \is_array($params['arguments'] ?? null) ? $params['arguments'] : [];
+
+ foreach ($this->tools->headersFor($params['name'], $arguments) as $suffix => $value) {
+ $headers[McpHeader::PARAM_PREFIX.$suffix] = $value;
+ }
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Mirrors W3C trace context from `_meta` onto its native HTTP headers, so
+ * an application that only knows how to set `_meta` — the transport-agnostic
+ * surface — still gets header-based propagation for free on HTTP.
+ *
+ * @param array|null $params
+ *
+ * @return array
+ */
+ private function traceHeaders(?array $params): array
+ {
+ $meta = \is_array($params['_meta'] ?? null) ? $params['_meta'] : [];
+
+ $headers = [];
+ foreach (RequestMeta::TRACE_KEYS as $key) {
+ if (\is_string($meta[$key] ?? null)) {
+ $headers[$key] = $meta[$key];
+ }
+ }
+
+ return $headers;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Stateless/InputRequestResolver.php b/admin/vendor/mcp/sdk/src/Client/Stateless/InputRequestResolver.php
new file mode 100644
index 0000000..a592542
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Stateless/InputRequestResolver.php
@@ -0,0 +1,204 @@
+
+ */
+final class InputRequestResolver
+{
+ /**
+ * The only requests a server may park in `inputRequests`. Anything else is
+ * a server fault, and answering it would be inventing protocol.
+ *
+ * @var array>
+ */
+ private const RESOLVABLE = [
+ 'elicitation/create' => ElicitRequest::class,
+ 'sampling/createMessage' => CreateSamplingMessageRequest::class,
+ 'roots/list' => ListRootsRequest::class,
+ ];
+
+ /**
+ * @param RequestHandlerInterface[] $handlers
+ */
+ public function __construct(
+ private readonly array $handlers,
+ private readonly LoggerInterface $logger = new NullLogger(),
+ ) {
+ }
+
+ /**
+ * Reads the `inputRequests` map off a result, or null when the result is
+ * not an ask.
+ *
+ * A result with no `resultType` MUST be read as complete, so an absent
+ * member is not an ask — only the explicit `input_required` is.
+ *
+ * @param array $result
+ *
+ * @return array|null
+ */
+ public static function asked(array $result): ?array
+ {
+ if (($result['resultType'] ?? null) !== 'input_required') {
+ return null;
+ }
+
+ return \is_array($result['inputRequests'] ?? null) ? $result['inputRequests'] : [];
+ }
+
+ /**
+ * Resolves every ask into the `inputResponses` map the retry carries.
+ *
+ * Keys are the server's, and each answer goes back under the key it was
+ * asked under — the client never reorders or renames them, since that map
+ * is how the server correlates answers to questions.
+ *
+ * @param array $inputRequests
+ *
+ * @return array
+ *
+ * @throws RuntimeException when an ask cannot be answered at all
+ */
+ public function resolve(array $inputRequests): array
+ {
+ $responses = [];
+
+ foreach ($inputRequests as $key => $ask) {
+ $responses[(string) $key] = $this->answer((string) $key, $ask);
+ }
+
+ return $responses;
+ }
+
+ /**
+ * @return array
+ */
+ private function answer(string $key, mixed $ask): array
+ {
+ if (!\is_array($ask) || !\is_string($ask['method'] ?? null)) {
+ throw new RuntimeException(\sprintf('Server asked for input under "%s" without a method to answer.', $key));
+ }
+
+ $method = $ask['method'];
+ $class = self::RESOLVABLE[$method] ?? null;
+
+ if (null === $class) {
+ throw new RuntimeException(\sprintf('Server asked for input under "%s" using "%s", which is not a request a client can answer.', $key, $method));
+ }
+
+ $params = $ask['params'] ?? null;
+ if ($params instanceof \stdClass) {
+ $params = (array) $params;
+ }
+
+ // The ask is a bare method/params pair, but the handlers speak in
+ // messages. The id never reaches the wire — the answer is keyed by
+ // $key — so any id will do.
+ $request = $class::fromArray([
+ 'jsonrpc' => '2.0',
+ 'id' => $key,
+ 'method' => $method,
+ 'params' => \is_array($params) ? $params : null,
+ ]);
+
+ $this->logger->debug('Resolving multi round-trip input request', [
+ 'key' => $key,
+ 'method' => $method,
+ ]);
+
+ $result = $this->dispatch($request);
+
+ if ($result instanceof Error) {
+ throw new RuntimeException(\sprintf('Cannot answer the server\'s "%s" input request under "%s": %s', $method, $key, $result->message));
+ }
+
+ return $result;
+ }
+
+ /**
+ * @return array|Error
+ */
+ private function dispatch(Request $request): array|Error
+ {
+ foreach ($this->handlers as $handler) {
+ if (!$handler->supports($request)) {
+ continue;
+ }
+
+ try {
+ $response = $handler->handle($request);
+ } catch (\Throwable $e) {
+ $this->logger->error('Input request handler failed', [
+ 'method' => $request::getMethod(),
+ 'exception' => $e,
+ ]);
+
+ return Error::forInternalError($e->getMessage(), $request->getId());
+ }
+
+ if ($response instanceof Error) {
+ return $response;
+ }
+
+ return self::resultOf($response);
+ }
+
+ return Error::forMethodNotFound(
+ \sprintf('Client does not handle "%s" requests.', $request::getMethod()),
+ $request->getId(),
+ );
+ }
+
+ /**
+ * @param Response $response
+ *
+ * @return array
+ */
+ private static function resultOf(Response $response): array
+ {
+ $result = $response->result;
+
+ if ($result instanceof \JsonSerializable) {
+ $result = $result->jsonSerialize();
+ }
+
+ if ($result instanceof \stdClass) {
+ $result = (array) $result;
+ }
+
+ return \is_array($result) ? $result : [];
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Stateless/RequestEnvelope.php b/admin/vendor/mcp/sdk/src/Client/Stateless/RequestEnvelope.php
new file mode 100644
index 0000000..fcc1cac
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Stateless/RequestEnvelope.php
@@ -0,0 +1,78 @@
+
+ */
+final class RequestEnvelope
+{
+ public function __construct(
+ private readonly ProtocolVersion $protocolVersion,
+ private readonly ClientCapabilities $capabilities,
+ private readonly Implementation $clientInfo,
+ ) {
+ }
+
+ public function protocolVersion(): ProtocolVersion
+ {
+ return $this->protocolVersion;
+ }
+
+ public function withProtocolVersion(ProtocolVersion $protocolVersion): self
+ {
+ return new self($protocolVersion, $this->capabilities, $this->clientInfo);
+ }
+
+ /**
+ * Merges the envelope into an encoded message, preserving whatever `_meta`
+ * the caller already put there — a `progressToken` most of all, which would
+ * otherwise be dropped and take every progress notification with it.
+ *
+ * @param array $payload a serialized JSON-RPC message
+ *
+ * @return array
+ */
+ public function stamp(array $payload): array
+ {
+ $params = \is_array($payload['params'] ?? null) ? $payload['params'] : [];
+ $meta = \is_array($params['_meta'] ?? null) ? $params['_meta'] : [];
+
+ $params['_meta'] = [
+ ...$meta,
+ RequestMeta::PROTOCOL_VERSION => $this->protocolVersion->value,
+ // ClientCapabilities encodes an empty set as `{}` already; `[]`
+ // would reach the server as a JSON array and fail its check.
+ RequestMeta::CLIENT_CAPABILITIES => $this->capabilities,
+ RequestMeta::CLIENT_INFO => $this->clientInfo,
+ ];
+
+ $payload['params'] = $params;
+
+ return $payload;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Stateless/ToolCatalog.php b/admin/vendor/mcp/sdk/src/Client/Stateless/ToolCatalog.php
new file mode 100644
index 0000000..3348db0
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Stateless/ToolCatalog.php
@@ -0,0 +1,206 @@
+
+ */
+final class ToolCatalog
+{
+ /** @var array> tool name to input schema */
+ private array $schemas = [];
+
+ /** @var array tool name to the reason it was refused */
+ private array $rejected = [];
+
+ public function __construct(
+ private readonly LoggerInterface $logger = new NullLogger(),
+ ) {
+ }
+
+ /**
+ * Records a listing page and returns the tools a caller may actually use.
+ *
+ * A tool whose annotations are malformed is dropped from the result, which
+ * is how the client "rejects" it: it never reaches the caller, so it cannot
+ * be called, and the tools listed beside it are untouched.
+ *
+ * @param list> $tools raw `tools/list` entries
+ *
+ * @return list>
+ */
+ public function record(array $tools): array
+ {
+ $usable = [];
+
+ foreach ($tools as $tool) {
+ $name = $tool['name'] ?? null;
+ $schema = $tool['inputSchema'] ?? null;
+
+ if (!\is_string($name) || !\is_array($schema)) {
+ $usable[] = $tool;
+
+ continue;
+ }
+
+ unset($this->rejected[$name], $this->schemas[$name]);
+
+ if (null !== $reason = McpHeader::checkAnnotations($schema)) {
+ $this->rejected[$name] = $reason;
+
+ $this->logger->warning('Excluding tool with an invalid "x-mcp-header" annotation', [
+ 'tool' => $name,
+ 'reason' => $reason,
+ ]);
+
+ continue;
+ }
+
+ $this->schemas[$name] = $schema;
+ $usable[] = $tool;
+ }
+
+ return $usable;
+ }
+
+ /**
+ * Whether the client refuses to call this tool.
+ *
+ * Only a tool that was listed and failed validation is refused; an unknown
+ * name is not, since the client may legitimately call a tool it never
+ * listed and the server is the authority on whether it exists.
+ */
+ public function isRejected(string $name): bool
+ {
+ return isset($this->rejected[$name]);
+ }
+
+ public function reasonFor(string $name): ?string
+ {
+ return $this->rejected[$name] ?? null;
+ }
+
+ /**
+ * The `Mcp-Param-*` headers a call to $name must carry, given its arguments.
+ *
+ * An argument that is absent or null contributes no header — the
+ * specification reads a missing header as a missing value, so sending an
+ * empty one would assert something different.
+ *
+ * @param array $arguments
+ *
+ * @return array
+ */
+ public function headersFor(string $name, array $arguments): array
+ {
+ $schema = $this->schemas[$name] ?? null;
+
+ if (null === $schema) {
+ return [];
+ }
+
+ $headers = [];
+
+ foreach (self::annotations($schema) as $header => $path) {
+ $value = self::valueAt($arguments, $path);
+
+ if (null === $value) {
+ continue;
+ }
+
+ $encoded = McpHeader::encode($value);
+
+ if (null === $encoded) {
+ continue;
+ }
+
+ $headers[$header] = $encoded;
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Every `x-mcp-header` annotation in $schema, as header name to the property
+ * path it mirrors.
+ *
+ * Only statically reachable properties count, matching the server's reader:
+ * a chain through `items`, a composition keyword or a `$ref` cannot be
+ * resolved without the instance, so an annotation there is out of bounds.
+ *
+ * @param array $schema
+ * @param list $path
+ *
+ * @return array>
+ */
+ private static function annotations(array $schema, array $path = []): array
+ {
+ $properties = $schema['properties'] ?? null;
+
+ if (!\is_array($properties)) {
+ return [];
+ }
+
+ $found = [];
+
+ foreach ($properties as $property => $definition) {
+ if (!\is_array($definition)) {
+ continue;
+ }
+
+ $here = [...$path, (string) $property];
+
+ if (\is_string($definition['x-mcp-header'] ?? null)) {
+ $found[$definition['x-mcp-header']] = $here;
+ }
+
+ $found = [...$found, ...self::annotations($definition, $here)];
+ }
+
+ return $found;
+ }
+
+ /**
+ * @param array $arguments
+ * @param list $path
+ */
+ private static function valueAt(array $arguments, array $path): mixed
+ {
+ $node = $arguments;
+
+ foreach ($path as $segment) {
+ if (!\is_array($node) || !\array_key_exists($segment, $node)) {
+ return null;
+ }
+
+ $node = $node[$segment];
+ }
+
+ return $node;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Transport/BaseTransport.php b/admin/vendor/mcp/sdk/src/Client/Transport/BaseTransport.php
new file mode 100644
index 0000000..a1fc193
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Transport/BaseTransport.php
@@ -0,0 +1,126 @@
+
+ */
+abstract class BaseTransport implements TransportInterface
+{
+ /** @var callable(): mixed|null */
+ protected $initializeCallback;
+
+ /** @var callable(string): void|null */
+ protected $messageCallback;
+
+ /** @var callable(\Throwable): void|null */
+ protected $errorCallback;
+
+ /** @var callable(string): void|null */
+ protected $closeCallback;
+
+ protected ?ClientStateInterface $state = null;
+ protected LoggerInterface $logger;
+
+ public function __construct(?LoggerInterface $logger = null)
+ {
+ $this->logger = $logger ?? new NullLogger();
+ }
+
+ public function onInitialize(callable $listener): void
+ {
+ $this->initializeCallback = $listener;
+ }
+
+ public function onMessage(callable $listener): void
+ {
+ $this->messageCallback = $listener;
+ }
+
+ public function onError(callable $listener): void
+ {
+ $this->errorCallback = $listener;
+ }
+
+ public function onClose(callable $listener): void
+ {
+ $this->closeCallback = $listener;
+ }
+
+ public function setState(ClientStateInterface $state): void
+ {
+ $this->state = $state;
+ }
+
+ /**
+ * Perform initialization via the registered callback.
+ *
+ * @return mixed The result from the initialization callback
+ *
+ * @throws RuntimeException If no initialize listener is registered
+ */
+ protected function handleInitialize(): mixed
+ {
+ if (!\is_callable($this->initializeCallback)) {
+ throw new RuntimeException('No initialize listener registered');
+ }
+
+ return ($this->initializeCallback)();
+ }
+
+ /**
+ * Handle an incoming message from the server.
+ */
+ protected function handleMessage(string $message): void
+ {
+ if (\is_callable($this->messageCallback)) {
+ try {
+ ($this->messageCallback)($message);
+ } catch (\Throwable $e) {
+ $this->handleError($e);
+ }
+ }
+ }
+
+ /**
+ * Handle a transport error.
+ */
+ protected function handleError(\Throwable $error): void
+ {
+ $this->logger->error('Transport error', ['exception' => $error]);
+
+ if (\is_callable($this->errorCallback)) {
+ ($this->errorCallback)($error);
+ }
+ }
+
+ /**
+ * Handle connection close.
+ */
+ protected function handleClose(string $reason): void
+ {
+ $this->logger->info('Transport closed', ['reason' => $reason]);
+
+ if (\is_callable($this->closeCallback)) {
+ ($this->closeCallback)($reason);
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Transport/HeaderAwareTransportInterface.php b/admin/vendor/mcp/sdk/src/Client/Transport/HeaderAwareTransportInterface.php
new file mode 100644
index 0000000..998a044
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Transport/HeaderAwareTransportInterface.php
@@ -0,0 +1,37 @@
+
+ */
+interface HeaderAwareTransportInterface extends TransportInterface
+{
+ /**
+ * Register the source of per-message headers.
+ *
+ * @param callable(string $payload): array $callback receives the encoded message
+ */
+ public function onHeaders(callable $callback): void;
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Transport/HttpTransport.php b/admin/vendor/mcp/sdk/src/Client/Transport/HttpTransport.php
new file mode 100644
index 0000000..b5499e8
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Transport/HttpTransport.php
@@ -0,0 +1,423 @@
+
+ */
+class HttpTransport extends BaseTransport implements HeaderAwareTransportInterface
+{
+ private ClientInterface $httpClient;
+ private RequestFactoryInterface $requestFactory;
+ private StreamFactoryInterface $streamFactory;
+
+ private ?string $sessionId = null;
+
+ /** @var (callable(string): array)|null */
+ private $headerCallback;
+
+ /** @var McpFiber|null */
+ private ?\Fiber $activeFiber = null;
+
+ /** @var (callable(float, ?float, ?string): void)|null */
+ private $activeProgressCallback;
+
+ /** @var StreamInterface|null Active SSE stream being read */
+ private ?StreamInterface $activeStream = null;
+
+ /** @var string Buffer for incomplete SSE data */
+ private string $sseBuffer = '';
+
+ /**
+ * Default cap on the bytes buffered while waiting for a complete SSE event.
+ */
+ public const DEFAULT_MAX_SSE_BUFFER_BYTES = 8 * 1024 * 1024;
+
+ private readonly int $maxSseBufferBytes;
+
+ /**
+ * @param string $endpoint The MCP server endpoint URL
+ * @param array $headers Additional headers to send
+ * @param ClientInterface|null $httpClient PSR-18 HTTP client (auto-discovered if null)
+ * @param RequestFactoryInterface|null $requestFactory PSR-17 request factory (auto-discovered if null)
+ * @param StreamFactoryInterface|null $streamFactory PSR-17 stream factory (auto-discovered if null)
+ * @param int $maxSseBufferBytes Maximum bytes buffered while waiting for a complete
+ * SSE event. A server that never sends the "\n\n" event
+ * delimiter would otherwise grow the buffer without bound
+ * and exhaust client memory; reaching the cap aborts the
+ * stream instead. Raise it for servers that legitimately
+ * emit single events larger than the default.
+ */
+ public function __construct(
+ private readonly string $endpoint,
+ private readonly array $headers = [],
+ ?ClientInterface $httpClient = null,
+ ?RequestFactoryInterface $requestFactory = null,
+ ?StreamFactoryInterface $streamFactory = null,
+ ?LoggerInterface $logger = null,
+ int $maxSseBufferBytes = self::DEFAULT_MAX_SSE_BUFFER_BYTES,
+ ) {
+ parent::__construct($logger);
+
+ if ($maxSseBufferBytes < 1) {
+ throw new InvalidArgumentException(\sprintf('The maximum SSE buffer size must be a positive number of bytes, got %d.', $maxSseBufferBytes));
+ }
+
+ $this->maxSseBufferBytes = $maxSseBufferBytes;
+ $this->httpClient = $httpClient ?? Psr18ClientDiscovery::find();
+ $this->requestFactory = $requestFactory ?? Psr17FactoryDiscovery::findRequestFactory();
+ $this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory();
+ }
+
+ public function connect(): void
+ {
+ $this->activeFiber = new \Fiber(fn () => $this->handleInitialize());
+
+ $this->activeFiber->start();
+
+ while (!$this->activeFiber->isTerminated()) {
+ $this->tick();
+ }
+
+ $result = $this->activeFiber->getReturn();
+ $this->activeFiber = null;
+
+ if ($result instanceof Error) {
+ throw new ConnectionException('Initialization failed: '.$result->message);
+ }
+
+ $this->logger->info('HTTP client connected and initialized', ['endpoint' => $this->endpoint]);
+ }
+
+ public function onHeaders(callable $callback): void
+ {
+ $this->headerCallback = $callback;
+ }
+
+ public function send(string $data): void
+ {
+ $request = $this->requestFactory->createRequest('POST', $this->endpoint)
+ ->withHeader('Content-Type', 'application/json')
+ ->withHeader('Accept', 'application/json, text/event-stream')
+ ->withBody($this->streamFactory->createStream($data));
+
+ if (null !== $this->sessionId) {
+ $request = $request->withHeader('Mcp-Session-Id', $this->sessionId);
+ }
+
+ // Protocol-derived first, so an explicitly configured header still wins:
+ // the caller passing one is making a deliberate choice about this
+ // connection, and a proxy credential is the usual reason.
+ foreach ($this->protocolHeaders($data) as $name => $value) {
+ $request = $request->withHeader($name, $value);
+ }
+
+ foreach ($this->headers as $name => $value) {
+ $request = $request->withHeader($name, $value);
+ }
+
+ $this->logger->debug('Sending HTTP request', ['data' => $data]);
+
+ try {
+ $response = $this->httpClient->sendRequest($request);
+ } catch (\Throwable $e) {
+ $this->handleError($e);
+ throw new ConnectionException('HTTP request failed: '.$e->getMessage(), 0, $e);
+ }
+
+ if ($response->hasHeader('Mcp-Session-Id')) {
+ $this->sessionId = $response->getHeaderLine('Mcp-Session-Id');
+ $this->logger->debug('Received session ID', ['session_id' => $this->sessionId]);
+ }
+
+ $contentType = strtolower($response->getHeaderLine('Content-Type'));
+
+ if (str_contains($contentType, 'text/event-stream')) {
+ $this->activeStream = $response->getBody();
+ $this->sseBuffer = '';
+ } elseif (str_contains($contentType, 'application/json')) {
+ $body = $response->getBody()->getContents();
+ if (!empty($body)) {
+ $this->handleMessage($body);
+ }
+ }
+ }
+
+ /**
+ * @param McpFiber $fiber
+ * @param (callable(float $progress, ?float $total, ?string $message): void)|null $onProgress
+ */
+ public function runRequest(\Fiber $fiber, ?callable $onProgress = null): Response|Error
+ {
+ $this->activeFiber = $fiber;
+ $this->activeProgressCallback = $onProgress;
+ $fiber->start();
+
+ while (!$fiber->isTerminated()) {
+ $this->tick();
+ }
+
+ $this->activeFiber = null;
+ $this->activeProgressCallback = null;
+ $this->activeStream = null;
+
+ return $fiber->getReturn();
+ }
+
+ public function close(): void
+ {
+ if (null !== $this->sessionId) {
+ try {
+ $request = $this->requestFactory->createRequest('DELETE', $this->endpoint)
+ ->withHeader('Mcp-Session-Id', $this->sessionId);
+
+ foreach ($this->headers as $name => $value) {
+ $request = $request->withHeader($name, $value);
+ }
+
+ $this->httpClient->sendRequest($request);
+ $this->logger->info('Session closed', ['session_id' => $this->sessionId]);
+ } catch (\Throwable $e) {
+ $this->logger->warning('Failed to close session', ['exception' => $e]);
+ }
+ }
+
+ $this->sessionId = null;
+ $this->activeStream = null;
+ $this->handleClose('Transport closed');
+ }
+
+ /**
+ * @return array
+ */
+ private function protocolHeaders(string $payload): array
+ {
+ if (!\is_callable($this->headerCallback)) {
+ return [];
+ }
+
+ try {
+ return ($this->headerCallback)($payload);
+ } catch (\Throwable $e) {
+ // Headers mirror the body; failing to derive them is a bug worth
+ // reporting, but dropping the request would be a worse outcome than
+ // sending it the way an earlier revision would have.
+ $this->logger->error('Could not derive protocol headers', ['exception' => $e]);
+
+ return [];
+ }
+ }
+
+ private function tick(): void
+ {
+ $this->processSSEStream();
+ $this->processProgress();
+ $this->processFiber();
+
+ usleep(1000); // 1ms
+ }
+
+ /**
+ * Read SSE data incrementally from active stream.
+ */
+ private function processSSEStream(): void
+ {
+ if (null === $this->activeStream) {
+ return;
+ }
+
+ if (!$this->activeStream->eof()) {
+ $chunk = $this->activeStream->read(4096);
+ if ('' !== $chunk) {
+ if (\strlen($this->sseBuffer) + \strlen($chunk) > $this->maxSseBufferBytes) {
+ $this->abortSseStream(\sprintf('buffered %d bytes without a complete event, exceeding the %d byte limit', \strlen($this->sseBuffer) + \strlen($chunk), $this->maxSseBufferBytes));
+
+ return;
+ }
+
+ $this->sseBuffer .= $chunk;
+ }
+ }
+
+ while (null !== ($event = $this->extractSSEEvent())) {
+ if (!empty(trim($event))) {
+ $this->processSSEEvent($event);
+ }
+ }
+
+ if ($this->activeStream->eof()) {
+ // The stream ended without a trailing blank line: dispatch what is left.
+ if (!empty(trim($this->sseBuffer))) {
+ $this->processSSEEvent($this->sseBuffer);
+ }
+
+ $this->sseBuffer = '';
+ $this->activeStream = null;
+ }
+ }
+
+ /**
+ * Tear down the active SSE stream and fail any in-flight request.
+ *
+ * The waiting fiber is resolved with an error immediately so the caller
+ * fails fast, rather than spinning until the request timeout elapses.
+ */
+ private function abortSseStream(string $reason): void
+ {
+ $bufferedBytes = \strlen($this->sseBuffer);
+ $this->sseBuffer = '';
+ $this->activeStream = null;
+
+ $this->logger->warning('Aborting SSE stream: '.$reason, [
+ 'session_id' => $this->sessionId,
+ 'buffered_bytes' => $bufferedBytes,
+ 'max_sse_buffer_bytes' => $this->maxSseBufferBytes,
+ ]);
+
+ if (null === $this->state) {
+ return;
+ }
+
+ foreach ($this->state->getPendingRequests() as $pending) {
+ $requestId = $pending['request_id'];
+ $error = Error::forInternalError('SSE stream aborted: '.$reason, $requestId);
+ $this->state->storeResponse($requestId, $error->jsonSerialize());
+ }
+ }
+
+ /**
+ * Take the next complete event off the buffer, or null if none is complete yet.
+ *
+ * Per the SSE specification, lines are terminated by CRLF, LF or CR, so an
+ * event is delimited by any pair of those. Servers built on sse-starlette
+ * (the MCP Python SDK) use CRLF.
+ */
+ private function extractSSEEvent(): ?string
+ {
+ $position = null;
+ $length = 0;
+
+ foreach (["\r\n\r\n", "\n\n", "\r\r"] as $delimiter) {
+ $found = strpos($this->sseBuffer, $delimiter);
+
+ if (false !== $found && (null === $position || $found < $position)) {
+ $position = $found;
+ $length = \strlen($delimiter);
+ }
+ }
+
+ if (null === $position) {
+ return null;
+ }
+
+ $event = substr($this->sseBuffer, 0, $position);
+ $this->sseBuffer = substr($this->sseBuffer, $position + $length);
+
+ return $event;
+ }
+
+ /**
+ * Parse a single SSE event and handle the message.
+ */
+ private function processSSEEvent(string $event): void
+ {
+ $data = '';
+
+ foreach (preg_split("/\r\n|\r|\n/", $event) ?: [] as $line) {
+ if (str_starts_with($line, 'data:')) {
+ $data .= trim(substr($line, 5));
+ }
+ }
+
+ if (!empty($data)) {
+ $this->handleMessage($data);
+ }
+ }
+
+ /**
+ * Process pending progress updates from session and execute callback.
+ */
+ private function processProgress(): void
+ {
+ if (null === $this->activeProgressCallback || null === $this->state) {
+ return;
+ }
+
+ $updates = $this->state->consumeProgressUpdates();
+
+ foreach ($updates as $update) {
+ try {
+ ($this->activeProgressCallback)(
+ $update['progress'],
+ $update['total'],
+ $update['message'],
+ );
+ } catch (\Throwable $e) {
+ $this->logger->warning('Progress callback failed', ['exception' => $e]);
+ }
+ }
+ }
+
+ private function processFiber(): void
+ {
+ if (null === $this->activeFiber || !$this->activeFiber->isSuspended()) {
+ return;
+ }
+
+ if (null === $this->state) {
+ return;
+ }
+
+ $pendingRequests = $this->state->getPendingRequests();
+
+ foreach ($pendingRequests as $pending) {
+ $requestId = $pending['request_id'];
+ $timestamp = $pending['timestamp'];
+ $timeout = $pending['timeout'];
+
+ $response = $this->state->consumeResponse($requestId);
+
+ if (null !== $response) {
+ $this->logger->debug('Resuming fiber with response', ['request_id' => $requestId]);
+ $this->activeFiber->resume($response);
+
+ return;
+ }
+
+ if (time() - $timestamp >= $timeout) {
+ $this->logger->warning('Request timed out', ['request_id' => $requestId]);
+ $error = Error::forInternalError('Request timed out', $requestId);
+ $this->activeFiber->resume($error);
+
+ return;
+ }
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Transport/StdioTransport.php b/admin/vendor/mcp/sdk/src/Client/Transport/StdioTransport.php
new file mode 100644
index 0000000..f102961
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Transport/StdioTransport.php
@@ -0,0 +1,339 @@
+
+ */
+class StdioTransport extends BaseTransport
+{
+ /** @var resource|null */
+ private $process;
+
+ /** @var resource|null */
+ private $stdin;
+
+ /** @var resource|null */
+ private $stdout;
+
+ /** @var resource|null */
+ private $stderr;
+
+ private string $inputBuffer = '';
+
+ /**
+ * Default cap on the bytes buffered while waiting for a complete line.
+ */
+ public const DEFAULT_MAX_BUFFER_SIZE = 4 * 1024 * 1024;
+
+ /** @var McpFiber|null */
+ private ?\Fiber $activeFiber = null;
+
+ /** @var (callable(float, ?float, ?string): void)|null */
+ private $activeProgressCallback;
+
+ /**
+ * @param string $command The command to run
+ * @param array $args Command arguments
+ * @param string|null $cwd Working directory
+ * @param array|null $env Environment variables
+ * @param int $maxBufferSize Maximum bytes buffered while waiting for a complete line. The
+ * buffer is only drained on a "\n"; a spawned server that streams
+ * stdout without ever emitting a newline would otherwise grow it
+ * without bound and exhaust client memory. Reaching the cap aborts
+ * the read instead. Raise it for servers that emit single frames
+ * larger than the default.
+ */
+ public function __construct(
+ private readonly string $command,
+ private readonly array $args = [],
+ private readonly ?string $cwd = null,
+ private readonly ?array $env = null,
+ ?LoggerInterface $logger = null,
+ private readonly int $maxBufferSize = self::DEFAULT_MAX_BUFFER_SIZE,
+ ) {
+ parent::__construct($logger);
+
+ if ($maxBufferSize < 1) {
+ throw new InvalidArgumentException(\sprintf('The maximum buffer size must be a positive number of bytes, got %d.', $maxBufferSize));
+ }
+ }
+
+ public function connect(): void
+ {
+ $this->spawnProcess();
+
+ $this->activeFiber = new \Fiber(fn () => $this->handleInitialize());
+
+ $this->activeFiber->start();
+
+ while (!$this->activeFiber->isTerminated()) {
+ $this->tick();
+ }
+
+ $result = $this->activeFiber->getReturn();
+ $this->activeFiber = null;
+
+ if ($result instanceof Error) {
+ $this->close();
+ throw new ConnectionException('Initialization failed: '.$result->message);
+ }
+
+ $this->logger->info('Client connected and initialized');
+ }
+
+ public function send(string $data): void
+ {
+ if (null === $this->stdin || !\is_resource($this->stdin)) {
+ throw new ConnectionException('Process stdin not available');
+ }
+
+ fwrite($this->stdin, $data."\n");
+ fflush($this->stdin);
+
+ $this->logger->debug('Sent message to server', ['data' => $data]);
+ }
+
+ /**
+ * @param McpFiber $fiber
+ * @param (callable(float $progress, ?float $total, ?string $message): void)|null $onProgress
+ */
+ public function runRequest(\Fiber $fiber, ?callable $onProgress = null): Response|Error
+ {
+ $this->activeFiber = $fiber;
+ $this->activeProgressCallback = $onProgress;
+ $fiber->start();
+
+ while (!$fiber->isTerminated()) {
+ $this->tick();
+ }
+
+ $this->activeFiber = null;
+ $this->activeProgressCallback = null;
+
+ return $fiber->getReturn();
+ }
+
+ public function close(): void
+ {
+ if (\is_resource($this->stdin)) {
+ fclose($this->stdin);
+ $this->stdin = null;
+ }
+ if (\is_resource($this->stdout)) {
+ fclose($this->stdout);
+ $this->stdout = null;
+ }
+ if (\is_resource($this->stderr)) {
+ fclose($this->stderr);
+ $this->stderr = null;
+ }
+ if (\is_resource($this->process)) {
+ proc_terminate($this->process, 15); // SIGTERM
+ proc_close($this->process);
+ $this->process = null;
+ }
+
+ $this->handleClose('Transport closed');
+ }
+
+ private function spawnProcess(): void
+ {
+ $descriptors = [
+ 0 => ['pipe', 'r'], // stdin
+ 1 => ['pipe', 'w'], // stdout
+ 2 => ['pipe', 'w'], // stderr
+ ];
+
+ $cmd = escapeshellcmd($this->command);
+ foreach ($this->args as $arg) {
+ $cmd .= ' '.escapeshellarg($arg);
+ }
+
+ $this->process = proc_open(
+ $cmd,
+ $descriptors,
+ $pipes,
+ $this->cwd,
+ $this->env
+ );
+
+ if (!\is_resource($this->process)) {
+ throw new ConnectionException('Failed to start process: '.$cmd);
+ }
+
+ $this->stdin = $pipes[0];
+ $this->stdout = $pipes[1];
+ $this->stderr = $pipes[2];
+
+ // Set non-blocking mode for reading
+ stream_set_blocking($this->stdout, false);
+ stream_set_blocking($this->stderr, false);
+
+ $this->logger->info('Started MCP server process', ['command' => $cmd]);
+ }
+
+ private function tick(): void
+ {
+ $this->processInput();
+ $this->processProgress();
+ $this->processFiber();
+ $this->processStderr();
+
+ usleep(1000); // 1ms
+ }
+
+ /**
+ * Process pending progress updates from session and execute callback.
+ */
+ private function processProgress(): void
+ {
+ if (null === $this->activeProgressCallback || null === $this->state) {
+ return;
+ }
+
+ $updates = $this->state->consumeProgressUpdates();
+
+ foreach ($updates as $update) {
+ try {
+ ($this->activeProgressCallback)(
+ $update['progress'],
+ $update['total'],
+ $update['message'],
+ );
+ } catch (\Throwable $e) {
+ $this->logger->warning('Progress callback failed', ['exception' => $e]);
+ }
+ }
+ }
+
+ private function processInput(): void
+ {
+ if (null === $this->stdout || !\is_resource($this->stdout)) {
+ return;
+ }
+
+ $data = fread($this->stdout, 8192);
+ if (false !== $data && '' !== $data) {
+ if (\strlen($this->inputBuffer) + \strlen($data) > $this->maxBufferSize) {
+ $this->abortInput(\sprintf('buffered %d bytes without a newline, exceeding the %d byte limit', \strlen($this->inputBuffer) + \strlen($data), $this->maxBufferSize));
+
+ return;
+ }
+
+ $this->inputBuffer .= $data;
+ }
+
+ while (false !== ($pos = strpos($this->inputBuffer, "\n"))) {
+ $line = substr($this->inputBuffer, 0, $pos);
+ $this->inputBuffer = substr($this->inputBuffer, $pos + 1);
+
+ $trimmed = trim($line);
+ if (!empty($trimmed)) {
+ $this->handleMessage($trimmed);
+ }
+ }
+ }
+
+ /**
+ * Discard the input buffer and fail any in-flight request.
+ *
+ * The waiting fiber is resolved with an error immediately so the caller
+ * fails fast, rather than spinning until the request timeout elapses.
+ */
+ private function abortInput(string $reason): void
+ {
+ $bufferedBytes = \strlen($this->inputBuffer);
+ $this->inputBuffer = '';
+
+ $this->logger->warning('Aborting stdio input: '.$reason, [
+ 'buffered_bytes' => $bufferedBytes,
+ 'max_buffer_size' => $this->maxBufferSize,
+ ]);
+
+ if (null === $this->state) {
+ return;
+ }
+
+ foreach ($this->state->getPendingRequests() as $pending) {
+ $requestId = $pending['request_id'];
+ $error = Error::forInternalError('stdio input aborted: '.$reason, $requestId);
+ $this->state->storeResponse($requestId, $error->jsonSerialize());
+ }
+ }
+
+ private function processFiber(): void
+ {
+ if (null === $this->activeFiber || !$this->activeFiber->isSuspended()) {
+ return;
+ }
+
+ if (null === $this->state) {
+ return;
+ }
+
+ $pendingRequests = $this->state->getPendingRequests();
+
+ foreach ($pendingRequests as $pending) {
+ $requestId = $pending['request_id'];
+ $timestamp = $pending['timestamp'];
+ $timeout = $pending['timeout'];
+
+ // Check if response arrived
+ $response = $this->state->consumeResponse($requestId);
+
+ if (null !== $response) {
+ $this->logger->debug('Resuming fiber with response', ['request_id' => $requestId]);
+ $this->activeFiber->resume($response);
+
+ return;
+ }
+
+ // Check timeout
+ if (time() - $timestamp >= $timeout) {
+ $this->logger->warning('Request timed out', ['request_id' => $requestId]);
+ $error = Error::forInternalError('Request timed out', $requestId);
+ $this->activeFiber->resume($error);
+
+ return;
+ }
+ }
+ }
+
+ private function processStderr(): void
+ {
+ if (null === $this->stderr || !\is_resource($this->stderr)) {
+ return;
+ }
+
+ $stderr = fread($this->stderr, 8192);
+ if (false !== $stderr && '' !== $stderr) {
+ $this->logger->debug('Server stderr', ['output' => trim($stderr)]);
+ }
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Client/Transport/TransportInterface.php b/admin/vendor/mcp/sdk/src/Client/Transport/TransportInterface.php
new file mode 100644
index 0000000..4860128
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Client/Transport/TransportInterface.php
@@ -0,0 +1,107 @@
+|Error)
+ * @phpstan-type FiberResume (Response|Error)
+ * @phpstan-type FiberSuspend array{type: 'await_response', request_id: int, timeout: int}
+ * @phpstan-type McpFiber \Fiber
+ *
+ * @author Kyrian Obikwelu
+ */
+interface TransportInterface
+{
+ /**
+ * Connect to the MCP server and perform initialization handshake.
+ *
+ * This method blocks until:
+ * - Initialization completes successfully
+ * - Connection fails (throws ConnectionException)
+ *
+ * @throws \Mcp\Exception\ConnectionException
+ */
+ public function connect(): void;
+
+ /**
+ * Send a message to the server immediately.
+ *
+ * @param string $data JSON-encoded message
+ */
+ public function send(string $data): void;
+
+ /**
+ * Run a request fiber to completion.
+ *
+ * The transport starts the fiber, runs its internal loop, and resumes
+ * the fiber when a response arrives or timeout occurs.
+ *
+ * During the loop, the transport checks session for progress data and
+ * executes the callback if provided.
+ *
+ * @param McpFiber $fiber The fiber to execute
+ * @param (callable(float $progress, ?float $total, ?string $message): void)|null $onProgress
+ * Optional callback for progress updates
+ *
+ * @return Response>|Error The response or error
+ */
+ public function runRequest(\Fiber $fiber, ?callable $onProgress = null): Response|Error;
+
+ /**
+ * Close the transport and clean up resources.
+ */
+ public function close(): void;
+
+ /**
+ * Register callback for initialization handshake.
+ *
+ * The callback should return a Fiber that performs the initialization.
+ *
+ * @param callable(): mixed $callback
+ */
+ public function onInitialize(callable $callback): void;
+
+ /**
+ * Register callback for incoming messages from server.
+ *
+ * @param callable(string $message): void $callback
+ */
+ public function onMessage(callable $callback): void;
+
+ /**
+ * Register callback for transport errors.
+ *
+ * @param callable(\Throwable $error): void $callback
+ */
+ public function onError(callable $callback): void;
+
+ /**
+ * Register callback for when connection closes.
+ *
+ * @param callable(string $reason): void $callback
+ */
+ public function onClose(callable $callback): void;
+
+ /**
+ * Set the client state for runtime state management.
+ */
+ public function setState(ClientStateInterface $state): void;
+}
diff --git a/admin/vendor/mcp/sdk/src/Event/ErrorEvent.php b/admin/vendor/mcp/sdk/src/Event/ErrorEvent.php
new file mode 100644
index 0000000..65ce650
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Event/ErrorEvent.php
@@ -0,0 +1,59 @@
+
+ */
+final class ErrorEvent
+{
+ public function __construct(
+ private Error $error,
+ private readonly Request $request,
+ private readonly SessionInterface $session,
+ private readonly ?\Throwable $throwable,
+ ) {
+ }
+
+ public function getError(): Error
+ {
+ return $this->error;
+ }
+
+ public function setError(Error $error): void
+ {
+ $this->error = $error;
+ }
+
+ public function getRequest(): Request
+ {
+ return $this->request;
+ }
+
+ public function getThrowable(): ?\Throwable
+ {
+ return $this->throwable;
+ }
+
+ public function getSession(): SessionInterface
+ {
+ return $this->session;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Event/NotificationEvent.php b/admin/vendor/mcp/sdk/src/Event/NotificationEvent.php
new file mode 100644
index 0000000..579d877
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Event/NotificationEvent.php
@@ -0,0 +1,51 @@
+
+ */
+final class NotificationEvent
+{
+ public function __construct(
+ private Notification $notification,
+ private readonly SessionInterface $session,
+ ) {
+ }
+
+ public function getNotification(): Notification
+ {
+ return $this->notification;
+ }
+
+ public function setNotification(Notification $notification): void
+ {
+ $this->notification = $notification;
+ }
+
+ public function getSession(): SessionInterface
+ {
+ return $this->session;
+ }
+
+ public function getMethod(): string
+ {
+ return $this->notification::getMethod();
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Event/PromptListChangedEvent.php b/admin/vendor/mcp/sdk/src/Event/PromptListChangedEvent.php
new file mode 100644
index 0000000..2e86918
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Event/PromptListChangedEvent.php
@@ -0,0 +1,19 @@
+
+ */
+final class PromptListChangedEvent
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Event/RequestEvent.php b/admin/vendor/mcp/sdk/src/Event/RequestEvent.php
new file mode 100644
index 0000000..3a7de08
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Event/RequestEvent.php
@@ -0,0 +1,51 @@
+
+ */
+final class RequestEvent
+{
+ public function __construct(
+ private Request $request,
+ private readonly SessionInterface $session,
+ ) {
+ }
+
+ public function getRequest(): Request
+ {
+ return $this->request;
+ }
+
+ public function setRequest(Request $request): void
+ {
+ $this->request = $request;
+ }
+
+ public function getSession(): SessionInterface
+ {
+ return $this->session;
+ }
+
+ public function getMethod(): string
+ {
+ return $this->request::getMethod();
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Event/ResourceListChangedEvent.php b/admin/vendor/mcp/sdk/src/Event/ResourceListChangedEvent.php
new file mode 100644
index 0000000..83120d6
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Event/ResourceListChangedEvent.php
@@ -0,0 +1,19 @@
+
+ */
+final class ResourceListChangedEvent
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Event/ResourceTemplateListChangedEvent.php b/admin/vendor/mcp/sdk/src/Event/ResourceTemplateListChangedEvent.php
new file mode 100644
index 0000000..0c13f65
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Event/ResourceTemplateListChangedEvent.php
@@ -0,0 +1,19 @@
+
+ */
+final class ResourceTemplateListChangedEvent
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Event/ResponseEvent.php b/admin/vendor/mcp/sdk/src/Event/ResponseEvent.php
new file mode 100644
index 0000000..94b9a90
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Event/ResponseEvent.php
@@ -0,0 +1,67 @@
+
+ */
+final class ResponseEvent
+{
+ /**
+ * @param Response $response
+ */
+ public function __construct(
+ private Response $response,
+ private readonly Request $request,
+ private readonly SessionInterface $session,
+ ) {
+ }
+
+ /**
+ * @return Response
+ */
+ public function getResponse(): Response
+ {
+ return $this->response;
+ }
+
+ /**
+ * @param Response $response
+ */
+ public function setResponse(Response $response): void
+ {
+ $this->response = $response;
+ }
+
+ public function getRequest(): Request
+ {
+ return $this->request;
+ }
+
+ public function getSession(): SessionInterface
+ {
+ return $this->session;
+ }
+
+ public function getMethod(): string
+ {
+ return $this->request::getMethod();
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Event/ToolListChangedEvent.php b/admin/vendor/mcp/sdk/src/Event/ToolListChangedEvent.php
new file mode 100644
index 0000000..84d175a
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Event/ToolListChangedEvent.php
@@ -0,0 +1,19 @@
+
+ */
+final class ToolListChangedEvent
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/BadMethodCallException.php b/admin/vendor/mcp/sdk/src/Exception/BadMethodCallException.php
new file mode 100644
index 0000000..d7c00c1
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/BadMethodCallException.php
@@ -0,0 +1,19 @@
+
+ */
+final class BadMethodCallException extends \BadMethodCallException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ClientException.php b/admin/vendor/mcp/sdk/src/Exception/ClientException.php
new file mode 100644
index 0000000..33e3668
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ClientException.php
@@ -0,0 +1,31 @@
+
+ */
+class ClientException extends Exception
+{
+ public function __construct(
+ private readonly Error $error,
+ ) {
+ parent::__construct($error->message);
+ }
+
+ public function getError(): Error
+ {
+ return $this->error;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ClientRegistrationException.php b/admin/vendor/mcp/sdk/src/Exception/ClientRegistrationException.php
new file mode 100644
index 0000000..3871146
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ClientRegistrationException.php
@@ -0,0 +1,23 @@
+
+ */
+class ConfigurationException extends InvalidArgumentException
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ConnectionException.php b/admin/vendor/mcp/sdk/src/Exception/ConnectionException.php
new file mode 100644
index 0000000..4e4527f
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ConnectionException.php
@@ -0,0 +1,21 @@
+
+ */
+class ConnectionException extends Exception
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ContainerException.php b/admin/vendor/mcp/sdk/src/Exception/ContainerException.php
new file mode 100644
index 0000000..0a56987
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ContainerException.php
@@ -0,0 +1,18 @@
+
+ */
+final class ElicitationException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/Exception.php b/admin/vendor/mcp/sdk/src/Exception/Exception.php
new file mode 100644
index 0000000..12ff5f0
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/Exception.php
@@ -0,0 +1,19 @@
+
+ */
+class Exception extends \Exception implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ExceptionInterface.php b/admin/vendor/mcp/sdk/src/Exception/ExceptionInterface.php
new file mode 100644
index 0000000..6344930
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ExceptionInterface.php
@@ -0,0 +1,19 @@
+
+ */
+interface ExceptionInterface extends \Throwable
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/HandlerNotFoundException.php b/admin/vendor/mcp/sdk/src/Exception/HandlerNotFoundException.php
new file mode 100644
index 0000000..ef222ed
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/HandlerNotFoundException.php
@@ -0,0 +1,19 @@
+
+ */
+class HandlerNotFoundException extends \InvalidArgumentException implements NotFoundExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/InvalidArgumentException.php b/admin/vendor/mcp/sdk/src/Exception/InvalidArgumentException.php
new file mode 100644
index 0000000..8c85a7c
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/InvalidArgumentException.php
@@ -0,0 +1,19 @@
+
+ */
+class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/InvalidCursorException.php b/admin/vendor/mcp/sdk/src/Exception/InvalidCursorException.php
new file mode 100644
index 0000000..5b08743
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/InvalidCursorException.php
@@ -0,0 +1,24 @@
+
+ */
+final class InvalidCursorException extends \InvalidArgumentException implements ExceptionInterface
+{
+ public function __construct(
+ public readonly string $cursor,
+ ) {
+ parent::__construct(\sprintf('Invalid value for pagination parameter "cursor": "%s"', $cursor));
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/InvalidInputMessageException.php b/admin/vendor/mcp/sdk/src/Exception/InvalidInputMessageException.php
new file mode 100644
index 0000000..0f96122
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/InvalidInputMessageException.php
@@ -0,0 +1,32 @@
+
+ */
+class InvalidInputMessageException extends \InvalidArgumentException implements ExceptionInterface
+{
+ private string|int|null $requestId = null;
+
+ public function getRequestId(): string|int|null
+ {
+ return $this->requestId;
+ }
+
+ public function setRequestId(string|int|null $requestId): self
+ {
+ $this->requestId = $requestId;
+
+ return $this;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/LogicException.php b/admin/vendor/mcp/sdk/src/Exception/LogicException.php
new file mode 100644
index 0000000..2ebe44c
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/LogicException.php
@@ -0,0 +1,19 @@
+
+ */
+final class LogicException extends \LogicException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/MissingRequestMetaException.php b/admin/vendor/mcp/sdk/src/Exception/MissingRequestMetaException.php
new file mode 100644
index 0000000..1499205
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/MissingRequestMetaException.php
@@ -0,0 +1,22 @@
+
+ */
+class MissingRequestMetaException extends InvalidArgumentException
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/MissingRequiredClientCapabilityException.php b/admin/vendor/mcp/sdk/src/Exception/MissingRequiredClientCapabilityException.php
new file mode 100644
index 0000000..aa8b031
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/MissingRequiredClientCapabilityException.php
@@ -0,0 +1,33 @@
+
+ */
+class MissingRequiredClientCapabilityException extends \RuntimeException implements ExceptionInterface
+{
+ public function __construct(
+ public readonly ClientCapabilities $requiredCapabilities,
+ string $message = 'Request requires a client capability that was not declared.',
+ ) {
+ parent::__construct($message);
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/NotFoundExceptionInterface.php b/admin/vendor/mcp/sdk/src/Exception/NotFoundExceptionInterface.php
new file mode 100644
index 0000000..faecad6
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/NotFoundExceptionInterface.php
@@ -0,0 +1,19 @@
+
+ */
+interface NotFoundExceptionInterface extends ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/PromptGetException.php b/admin/vendor/mcp/sdk/src/Exception/PromptGetException.php
new file mode 100644
index 0000000..7eec0da
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/PromptGetException.php
@@ -0,0 +1,19 @@
+
+ */
+final class PromptGetException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/PromptNotFoundException.php b/admin/vendor/mcp/sdk/src/Exception/PromptNotFoundException.php
new file mode 100644
index 0000000..81b7c6e
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/PromptNotFoundException.php
@@ -0,0 +1,24 @@
+
+ */
+final class PromptNotFoundException extends \RuntimeException implements NotFoundExceptionInterface
+{
+ public function __construct(
+ public readonly string $name,
+ ) {
+ parent::__construct(\sprintf('Prompt not found: "%s".', $name));
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/RegistryException.php b/admin/vendor/mcp/sdk/src/Exception/RegistryException.php
new file mode 100644
index 0000000..c483a01
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/RegistryException.php
@@ -0,0 +1,35 @@
+
+ */
+class RequestException extends Exception
+{
+ private ?Error $error;
+
+ public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, ?Error $error = null)
+ {
+ parent::__construct($message, $code, $previous);
+ $this->error = $error;
+ }
+
+ public static function fromError(Error $error): self
+ {
+ return new self($error->message, $error->code, null, $error);
+ }
+
+ public function getError(): ?Error
+ {
+ return $this->error;
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/RequestStateException.php b/admin/vendor/mcp/sdk/src/Exception/RequestStateException.php
new file mode 100644
index 0000000..2dde880
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/RequestStateException.php
@@ -0,0 +1,24 @@
+
+ */
+class RequestStateException extends InvalidArgumentException
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ResourceNotFoundException.php b/admin/vendor/mcp/sdk/src/Exception/ResourceNotFoundException.php
new file mode 100644
index 0000000..420ac1a
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ResourceNotFoundException.php
@@ -0,0 +1,24 @@
+
+ */
+final class ResourceNotFoundException extends \RuntimeException implements NotFoundExceptionInterface
+{
+ public function __construct(
+ public readonly string $uri,
+ ) {
+ parent::__construct(\sprintf('Resource not found for uri: "%s".', $uri));
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ResourceReadException.php b/admin/vendor/mcp/sdk/src/Exception/ResourceReadException.php
new file mode 100644
index 0000000..a89dec8
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ResourceReadException.php
@@ -0,0 +1,19 @@
+
+ */
+final class ResourceReadException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/RootsException.php b/admin/vendor/mcp/sdk/src/Exception/RootsException.php
new file mode 100644
index 0000000..8d12cb0
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/RootsException.php
@@ -0,0 +1,24 @@
+
+ */
+final class RootsException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/RuntimeException.php b/admin/vendor/mcp/sdk/src/Exception/RuntimeException.php
new file mode 100644
index 0000000..b68a1bf
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/RuntimeException.php
@@ -0,0 +1,19 @@
+
+ */
+final class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/SamplingException.php b/admin/vendor/mcp/sdk/src/Exception/SamplingException.php
new file mode 100644
index 0000000..17abceb
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/SamplingException.php
@@ -0,0 +1,24 @@
+
+ */
+final class SamplingException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ServiceNotFoundException.php b/admin/vendor/mcp/sdk/src/Exception/ServiceNotFoundException.php
new file mode 100644
index 0000000..2d22708
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ServiceNotFoundException.php
@@ -0,0 +1,18 @@
+
+ */
+class TimeoutException extends Exception
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ToolCallException.php b/admin/vendor/mcp/sdk/src/Exception/ToolCallException.php
new file mode 100644
index 0000000..01ba9f4
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ToolCallException.php
@@ -0,0 +1,19 @@
+
+ */
+final class ToolCallException extends \RuntimeException implements ExceptionInterface
+{
+}
diff --git a/admin/vendor/mcp/sdk/src/Exception/ToolNotFoundException.php b/admin/vendor/mcp/sdk/src/Exception/ToolNotFoundException.php
new file mode 100644
index 0000000..0a864e7
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/Exception/ToolNotFoundException.php
@@ -0,0 +1,24 @@
+
+ */
+final class ToolNotFoundException extends \RuntimeException implements NotFoundExceptionInterface
+{
+ public function __construct(
+ public readonly string $name,
+ ) {
+ parent::__construct(\sprintf('Tool not found: "%s".', $name));
+ }
+}
diff --git a/admin/vendor/mcp/sdk/src/JsonRpc/MessageFactory.php b/admin/vendor/mcp/sdk/src/JsonRpc/MessageFactory.php
new file mode 100644
index 0000000..27860a2
--- /dev/null
+++ b/admin/vendor/mcp/sdk/src/JsonRpc/MessageFactory.php
@@ -0,0 +1,216 @@
+
+ * @author Kyrian Obikwelu
+ */
+final class MessageFactory
+{
+ /**
+ * Registry of all known message classes that have methods.
+ *
+ * @var list|class-string>
+ */
+ private const REGISTERED_MESSAGES = [
+ Schema\Notification\CancelledNotification::class,
+ Schema\Notification\InitializedNotification::class,
+ Schema\Notification\LoggingMessageNotification::class,
+ Schema\Notification\ProgressNotification::class,
+ Schema\Notification\PromptListChangedNotification::class,
+ Schema\Notification\ResourceListChangedNotification::class,
+ Schema\Notification\ResourceUpdatedNotification::class,
+ Schema\Notification\RootsListChangedNotification::class,
+ Schema\Notification\ToolListChangedNotification::class,
+
+ Schema\Request\CallToolRequest::class,
+ Schema\Request\CompletionCompleteRequest::class,
+ Schema\Request\CreateSamplingMessageRequest::class,
+ Schema\Request\ElicitRequest::class,
+ Schema\Request\GetPromptRequest::class,
+ Schema\Request\InitializeRequest::class,
+ Schema\Request\ListPromptsRequest::class,
+ Schema\Request\ListResourcesRequest::class,
+ Schema\Request\ListResourceTemplatesRequest::class,
+ Schema\Request\ListRootsRequest::class,
+ Schema\Request\ListToolsRequest::class,
+ Schema\Request\PingRequest::class,
+ Schema\Request\ReadResourceRequest::class,
+ Schema\Request\ResourceSubscribeRequest::class,
+ Schema\Request\ResourceUnsubscribeRequest::class,
+ Schema\Request\SetLogLevelRequest::class,
+ ];
+
+ /**
+ * Upper bound on the number of messages accepted in a single batch, guarding
+ * against amplification where one small request expands into many operations.
+ */
+ public const DEFAULT_MAX_BATCH_SIZE = 100;
+
+ /**
+ * @param list