MCP: emit inputSchema.properties as an object, not [] (breaks strict clients like the Claude CLI)#2017
Open
MWest2020 wants to merge 1500 commits into
Open
MCP: emit inputSchema.properties as an object, not [] (breaks strict clients like the Claude CLI)#2017MWest2020 wants to merge 1500 commits into
MWest2020 wants to merge 1500 commits into
Conversation
Pre-existing t() source strings (source/EditSource, templates, LLM settings) were used in Vue but absent from l10n/en.json, failing the l10n-check gate. Extracted via `check-l10n.js --write` (key === English source). Reduces carried l10n drift on development; the separate 36-locale parity backlog for these keys remains pre-existing and out of scope for this PR.
- PHPCS: fix param-doc/inline-if/inline-comment/function-spacing across the federation controller, provider, listener, flow service and Application boot wiring (phpcbf + manual); add @SPEC exclude tags on the OCM listener. - PHPStan: match ICloudFederationProvider::notificationReceived signature (untyped params, array<array-key,string> return); drop a redundant ?? on a non-nullable getShareSecret(); baseline the two QBMapper generic-variance entries for FederatedShareMapper insert()/update() (same pattern already baselined for SourceMapper). PHPCS 0 errors, PHPStan OK, unit 17/17 green the CI way.
FunctionSpacing: collapse double blank lines between members to one across the federation entity, mapper, share service and object-source provider, matching the standard applied to the rest of the feature.
The DBAL-virtual-registers frontend strings (source/EditSource, templates, LLM settings) were present in used code + en.json but missing from every locale, failing the l10n-parity hard gate. Backfill each required locale's .json with the English source value (non-empty; parity forbids missing/empty). Clears the carried l10n-parity debt so app-tests.yml goes green. English placeholders pending real translation via the normal l10n pipeline.
…only `::text` with dialect-aware CAST(_ AS CHAR) (WOO-520) ## Symptoom `GET /apps/opencatalogi/api/search` crashte op MariaDB backends met: ``` SQLSTATE[42000]: Syntax error near '::text AS _id, _uuid::text AS _uuid, _slug::text AS _slug, _uri::text AS _uri...' at line 1 ``` De crash zit in `MagicMapper::buildUnionSelectPart()` — dat is de per-schema UNION-arm die opgebouwd wordt zodra een consumer een multi-schema search uitvoert (`_schemas: [id1, id2]`). WOO-506's SCH-PFTS endpoint doet precies dat vanaf commit 2387a7b7 (PR #88 op opencatalogi): het roept `ObjectService::searchObjectsPaginated()` aan met beide `publication` en `document` schema-ids, waarna deze UNION-builder de crash triggert. ## Root cause De cast-syntax `col::text` is PostgreSQL-only. MariaDB/MySQL vereisen de standaard `CAST(col AS CHAR)`. Op de PROPERTY-column projection (lines 1490-1512) was al platform-aware code aanwezig (via `$isPostgres` flag). De METADATA-column projection erboven (lines 1436-1458) én de NULL-column placeholders waren dat NIET — die gebruikten onvoorwaardelijk `::text`. Hydra CI runt op PostgreSQL → geen enkele gate ving deze in. Elke local dev-env op MariaDB brak zodra WOO-506's endpoint gehit werd. ## Fix Zelfde platform-switch pattern als bij de property-column loop: ```php $colExpr = "CAST({$metaCol} AS CHAR) AS {$metaCol}"; if ($isPostgres === true) { $colExpr = "{$metaCol}::text AS {$metaCol}"; } ``` Toegepast op: - `_metadataColumns` SELECT expressies (existing + NULL placeholders) `$isPostgres` is al gedefinieerd bovenaan `buildUnionSelectPart()` (line 1422) uit `stripos($platform::class, 'PostgreSQL')` — de fix hergebruikt dat. ## Out of scope voor deze fix De volgende PG-only expressies in dezelfde method (`buildUnionSelectPart()`) zijn score-berekeningen die alleen firen bij `_search` param: - Line 1557: `{$quotedCol}::text ILIKE {$likePattern}` — `ILIKE` is ook PG-only - Line 1559: `similarity({$quotedCol}::text, {$quotedTerm})` — `similarity()` is een PG extension (pg_trgm) Deze breken een `?_search=X` query op MariaDB (niet de base list-query die WOO-520 reporte). Zijn een bredere portability-refactor waard — voorstel: losse follow-up subtask op WOO-506 die de search-scoring dialect-agnostisch maakt (bijv. via `MATCH...AGAINST` op MySQL / plaintext LIKE-fallback). ## Test-plan - [ ] `php -l lib/Db/MagicMapper.php` → clean (verified) - [ ] Op MariaDB dev-env (nextcloud-docker-dev met `database-mysql`): `GET /apps/opencatalogi/api/search` returnt HTTP 200 met `{"results":[...], "total":N}` in plaats van HTTP 500 - [ ] Op PostgreSQL (Hydra CI): unchanged behavior — `$isPostgres` branch levert nog steeds `col::text` SQL - [ ] Bestaande PHPUnit-tests voor MagicMapper blijven groen ## Follow-ups - **`_search` param op MariaDB** — nog steeds broken (ILIKE + similarity()); losse subtask onder WOO-506 - **Andere PG-only spots** (95 hits total in `lib/`) — bredere OR-portability refactor; buiten scope Closes WOO-520. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Picks up the Files widget upload UX (card chrome on detail pages, upload default-on, drag-and-drop) and the page-config modal view-mode/map config (nextcloud-vue beta.168).
….0.0-beta.168' (#339) from chore/ncvue-beta168 into development
…chival-transfer-hardening, dsar-escalation-and-dpia, semantic-object-handoff-engine); branch code superseded by squash merges
…rest of branch superseded)
… casts in UNION query
Nextcloud 34's app-management page inlines the raw app.svg and recolors it via 'fill: currentcolor' on the <svg> element. A fill carried on <path>/<style>/class beats the inherited value, so the icon rendered white-on-white and vanished from the list. Moving the fill to the <svg> element lets Nextcloud recolor the icon; visuals are unchanged everywhere the icon is shown on a dark/colored surface.
…currentcolor recoloring' (#341) from fix/app-icon-svg-level-fill into development
setSelfMetadata() now applies a client-supplied @self.geo onto the object entity via setGeo(). Unlike organisation/tmlo/folder this is plain descriptive geographical metadata that drives no security or lifecycle decision, so it is client-writable; an explicit null clears it. This is the backend for map widgets (CnObjectGeoWidget) that edit an object's location via a minimal PATCH of @self.geo only.
…e (map widget backend)' (#343) from feat/object-geo-metadata into development
The published css/index.css re-exported only the unscoped globals, and webpack pruned the ESM barrel's CSS import as side-effect-free, so any build resolving the npm dist shipped ZERO scoped SFC CSS. Components rendered data-v-* scope attributes with no matching rules and silently lost every scoped declaration — flex containers fell back to display: block. Fixed upstream in nextcloud-vue#171 (css/index.css now @imports the built stylesheet) and #173 (wrapped dashboard header actions stay right-aligned). Local builds masked the bug for a long time: they alias @conduction/nextcloud-vue to the sibling ../nextcloud-vue/src checkout and compile the SFCs (and their scoped styles) in-app. CI and production have no sibling, so they always resolved the dist — and always shipped the broken CSS. This app had no way to reproduce that: `useLocalLib` was hard-wired to `fs.existsSync(localLib)`, so a developer with a sibling checkout could never build what CI builds. Add the fleet-standard `USE_LOCAL_LIB=false` escape hatch.
`nextcloud/ocp` is a require-dev STUB package (NC API signatures for Psalm /
PHPStan). It was mapped in the RUNTIME `autoload.psr-4`:
"OCP\\": "vendor/nextcloud/ocp/OCP/",
"NCU\\": "vendor/nextcloud/ocp/NCU/"
and lib/AppInfo/Application.php requires vendor/autoload.php — so every request
that touched OpenRegister registered a psr-4 map sending `OCP\*` to the stubs.
The stubs are pinned `^31.0`. On this NC 34 instance that means core's
`OC\Settings\Manager` (which carries `#[\Override]` on `getAdminDelegatedSettings()`)
was compiled against the NC 31 `OCP\Settings\IManager`, which predates that
method. PHP 8.3 raises a COMPILE-TIME fatal:
OC\Settings\Manager::getAdminDelegatedSettings() has #[\Override] attribute,
but no matching parent method exists
That takes down the WHOLE instance, not just this app: `occ` dies, `occ app:list`
returns 0 apps, and every /apps/openregister/api/* route 404s while other apps'
endpoints 500. Observed today — pipelinq's dashboard lost all of its KPI and
chart data because the aggregation endpoints were unreachable.
Moves the stub mapping to `autoload-dev`, where static analysis still resolves it
and a `--no-dev` deploy cannot leak it into the runtime autoloader.
NOTE — this is only half the mitigation. `autoload-dev` is STILL baked into the
generated autoloader by a plain `composer install` (dev mode). In this dev
topology the app checkout IS the served app, so a routine `composer install` here
re-bricks the instance. Same landmine in hermiq and launchpad. A durable fix is
either always deploying `--no-dev`, or pinning `nextcloud/ocp` to the running NC
major so a leak is harmless.
…runtime
Moving the stub mapping to `autoload-dev` (previous commit) narrowed the failure
window but did not close it: `autoload-dev` is STILL baked into the generated
autoloader by a plain `composer install`. In this topology the app checkout IS
the served app, so a routine dev install would re-brick the instance — which is
exactly how it broke today.
The mapping turns out to be entirely redundant. Both analysers already discover
the stubs through their own mechanisms, neither of which consults Composer's
autoloader:
phpstan.neon: scanDirectories: - vendor/nextcloud/ocp
psalm.xml: <extraFiles><directory name="vendor/nextcloud/ocp" /></extraFiles>
plus an explicit <stubs> entry
Verified with the mapping removed:
- `composer dump-autoload` in DEV mode emits ZERO references to nextcloud/ocp
in autoload_psr4.php / autoload_static.php → OCP can no longer leak into the
runtime autoloader under ANY install mode.
- Psalm: 124 errors, ZERO of which mention OCP. All 96 UndefinedClass errors
are Doctrine\DBAL\* and Sabre\VObject\* (NC core 3rdparty), pre-existing and
unrelated. So OCP still resolves for static analysis.
This removes the failure mode rather than narrowing it, and needs no discipline
(`--no-dev`) and no coupling to the running NC major.
…rphans
Deleting a schema that still holds objects was broken in several ways. Diagnosed
live: a "Cow" schema with one object refused to delete (correctly, 409) but the
refusal was unreadable, there was no clean way to delete it, and the UI's
delete-then-unlink ordering corrupted the register on the way past.
## The cascade
`DELETE /api/schemas/{id}?deleteObjects=true` — audit + hard-delete every object,
drop the now-empty magic table(s), delete the schema. Mutually exclusive with
`force` (both -> 400 conflicting-delete-dispositions): force ORPHANS the objects,
deleteObjects REMOVES them; an ambiguous destructive intent is refused rather
than guessed. The no-flag path is byte-for-byte unchanged (409 + objectCount),
and `force` still behaves exactly as before for API clients.
Two phases, because DROP TABLE is DDL and implicitly commits on MySQL/MariaDB
(it cannot be rolled back): phase 1 is one transaction (audit + rows + schema);
phase 2 drops the tables post-commit, best-effort. A failed drop logs a WARNING
and returns success with `tableDropped: false` — it never fails a request whose
data work already succeeded, and never pretends to roll back what it cannot.
A schema can live in MULTIPLE registers, each with its own magic table, and the
guard counts across all of them — so the cascade iterates every one of them.
Dropping only the "current" table would leave behind orphans the guard had
already counted.
## Bugs found and fixed on the way
- `ObjectService::deleteObjectsBySchema()` was a THROWING STUB, so
`POST /api/bulk/{register}/{schema}/delete-objects` had been returning HTTP 500.
The MagicMapper method it needed was already fully implemented — it just was
never wired up. Both bulk routes work again; the misleadingly-named
`bulk#deleteSchema` (which deletes objects, not a schema) is marked @deprecated.
- `MagicMapper::dropTable()` NEVER WORKED. It called
`ConnectionAdapter::quoteIdentifier()`, which does not exist on this Nextcloud —
so it threw "Call to undefined method" on every single call. That is why deleted
schemas always left their magic table behind. Now quotes via the platform, the
pattern the rest of the codebase already uses.
- `SchemaMapper::delete()`'s "objects still attached" guard queried the RETIRED
`openregister_objects` blob table, which is always empty for magic-table
objects. It counted 0 and waved everything through — a dead guard. Re-pointed at
the magic tables (direct query: MagicStatisticsHandler injects SchemaMapper, so
injecting it back would be circular) and given an explicit `force` bypass that
only the `?force=true` disposition may pass.
The three callers behind that dead guard now genuinely refuse. Two of them —
SchemasToolProvider and SchemaTool — are LLM-invokable with no controller in
front: a model acting on an ambiguous instruction must not be able to orphan
data. TablesSchemaSyncService also refuses; it retires read-only mirrors whose
objects live in Nextcloud Tables, so the repaired guard counts 0 and retirement
proceeds unchanged — and if a magic table DOES hold rows, that is real data the
sync job has no mandate to destroy.
- `TablesSchemaSyncService::retireMissing()` unlinked a schema from its register
BEFORE deleting it, and left it unlinked when the delete threw — the exact
orphaned-schema corruption this change exists to close.
- `BulkController` passed `hardDelete` unnormalized (a query/form request delivers
the string "true") into a bool-typed parameter: a latent TypeError that would
have surfaced the moment the stub was fixed.
Live-verified: no-flag 409 unchanged; both flags 400; cascade leaves 0 rows,
0 tables, 0 schema; bulk endpoint 200; 0-object cascade fine.
Tests: 19 new (SchemaDeletionService, SchemaMapper guard, destroy safety).
PHPUnit/Psalm/PHPStan/PHPMD identical to the origin/development baseline — zero
new issues. Also fixed 57 pre-existing PHPCS errors across 8 untouched files.
…its objects, leaving no orphans
The previous commit removed the `OCP\` / `NCU\` psr-4 mapping from composer.json,
which is what let the nextcloud/ocp stubs shadow core's OCP and take the whole
instance down. But the mapping was NOT purely redundant, as I first thought: CI
runs PHPUnit standalone on php:8.3-cli with no Nextcloud, and tests/bootstrap.php
loads only vendor/autoload.php — so OCP\* had nowhere else to come from.
Move the registration to the test entry point, where it belongs: a dev-only
mapping in the dev-only bootstrap, instead of in composer.json where a plain
`composer install` bakes it into the autoloader of the SERVED app.
- CI / standalone: bootstrap registers the stubs when no live NC supplies OCP.
- Served app: composer's autoloader has no OCP mapping under ANY install mode,
so it can never shadow core.
- Static analysis: unaffected — PHPStan reads the stubs via `scanDirectories`,
Psalm via `<extraFiles>`.
Registered AFTER the Doctrine stubs: IQueryBuilder declares constants referencing
Doctrine\DBAL\ParameterType, evaluated as the file is parsed, so the placeholders
must already be in the class table.
Verified against a CI-identical run (php:8.3-cli, no Nextcloud), before vs after:
baseline (mapping in composer): 14058 tests, 115 errors, 17 failures
this change (bootstrap): 14058 tests, 115 errors, 17 failures
Byte-for-byte identical — the pre-existing failures are untouched and nothing
regressed.
…tubs at runtime — they were bricking the whole instance' (#346) from fix/ocp-stubs-not-runtime-autoload into development
…enBuild PAT custody) (#351)
…, trust-config repair Snapshot of uncommitted work that was sitting in the shared dev checkout, committed so it is captured in git (and pushable) before merging origin/development in. The working tree is byte-identical before and after this commit — nothing is rewritten. Contains: - PublicApiCorsMiddleware registration + #[AnonRateLimit] on object create, and the public-create RBAC path in PermissionHandler (+ tests) - lib/Repair/ImportTrustConfigurationRegister.php (new) - credential-provider catalogue additions, EditSchema.vue option enrichment Several of these files had already been re-derived upstream and are byte-identical to origin/development; the merge that follows reconciles them.
…ta190 # Conflicts: # package-lock.json
…ary drift CWE-863: getRegisterAuthorization() swallowed \Throwable, returned null without logging, and cached that null. Callers read empty(null) as 'no rules configured' and granted FULL permissions -- permanently, for the request. - Authorization resolvers now throw AuthorizationUnresolvableException and never cache a failure as an answer; every caller routes it to a deny. - getRegisterForSchema() had the same shape (it logged, but logging a fail-open does not make it safe) -- also fails closed now. - MagicRbacHandler clamps to the deny-all predicate instead of 'open to all'. - Vocabulary: drop phantom x-openregister-seed (no engine); add x-openregister-processing (read by ProcessingLogService, was dropped). - Relocate the 6 MDM trust rules to components.objects so they actually plant.
An unconfigured ContainerInterface mock returned null for RegisterMapper, so calling a method on it raised an Error the old resolver swallowed into 'open'. These tests are about custom-scope listener voting; they must not depend on a fail-open to reach the listener path.
…ed + vocabulary requirements
…ift (apply)' (#441) from wip/authz-fail-closed-and-vocabulary-drift into development
…s: done) Canonical authorization-rbac spec carries the three requirements (synced in #441): fail-closed authorization resolution, declared seed data is planted, and the annotation vocabulary contains only engine-backed keys.
…ulary-drift' (#442) from wip/archive-authz-fail-closed into development
…osed-and-vocabulary-drift' (#443) from wip/scratchpad-authz-report into development
…vider Hermiq needs to run a Claude Max/Pro subscription through the official `claude` CLI, which is the ToS-sanctioned path: Anthropic categorically refuses a subscription OAuth token on the direct Messages API (verified 2026-07-16 — HTTP 429 rate_limit_error carrying anthropic-organization-id but NO retry-after and NO anthropic-ratelimit-* counters, byte-identical after 14h of zero usage; a refusal, not a quota). A CLI reads its token from the process ENVIRONMENT. There is no outbound request for the broker to proxy and no header for injectAuth() to substitute into, so the constrained-proxy path cannot express this credential at any host — the host (api.anthropic.com) is perfectly well known. resolveInjectable() correctly returns null for the host-locked anthropic-oauth proxy provider, so today Hermiq cannot obtain the token at all. CredentialController::create() also rejects an unregistered provider with a 400, so a user cannot even save the credential. Adds anthropic-cli: inject_only, no baseUrl, no allowRules. Grouped with the other anthropic providers — both broker branches key on the flag (isInjectOnly()), never on position, so grouping is editorial and a Claude provider should not hide inside a block titled 'generic'. Also generalises $injectOnlyComment. It read as a narrative about OpenConnector's unbounded hosts and said 'the five entries', so a sixth inject-only entry elsewhere would silently falsify the reader's inference that inject_only implies generic-*. inject_only now states the general rule: the broker cannot bound this call, so it refuses to make it — which covers both an unbounded host and a non-HTTP consumer. No PHP: request() already denies inject-only providers and resolveInjectable() already releases only for them, both strictly after Guard 1 (owner/IDOR) and Guard 2 (allowedApps). The new entry inherits correct handling from the flag alone. Version 1.5.0 -> 1.6.0 (6473d7e already took 1.5.0 for the anthropic descriptors; the $injectOnlyComment's 1.4.0 narrative is stale). The trade-off is deliberate and bounded: an inject-only secret leaves OpenRegister into the trusted same-instance app. Forced (a CLI needs it in its env), precedented (the five generic-* entries), and bounded (both guards still run; Doriath keeps custody; app config holds only a credentialRef). Prefer the zero-knowledge anthropic proxy entry wherever it works. Claude Max/Pro is PERSONAL-SCOPE ONLY per the Anthropic ToS — declared here, enforced by the consuming app which owns the only resolution path.
…al provider' (#445) from feat/anthropic-cli-provider into development
…r trusted in-process callers (#450) ADR-064 Rule 4 requires an infrastructure credential (a source/consumer secret) to be organisation-scoped, never personal. But its trusted in-process consumers — openconnector's migration repair step (Phase C) and background sync jobs — run with NO user session, and the original design D3 "no session denies" rule left org-scoped infrastructure credentials unresolvable. Add a sessionless organisation assertion mirroring the existing actingUserId posture (honored ONLY without a session; never settable from request input): - resolveInjectable() gains ?string $actingOrganisationId as its last param, threaded through loadAdmittedCredential() into assertOrganisationMember(). - assertOrganisationMember() now admits on two paths: with a session the session is authoritative and the asserted value is IGNORED (a request-context caller can never escalate via the assertion); sessionless, it admits iff actingOrganisationId equals the credential's organisation. Matching keeps resolution decoupled from any individual user's membership (the point of org scope per ADR-064) — the actingUserId fallback is deliberately not reused. Trust boundary: resolveInjectable() is NOT HTTP-routed, so the assertion is only ever set by in-process code. The routed request() passes null explicitly (comment records the invariant), and CredentialController never reads it — mirroring how it already refuses to forward actingUserId. Tests: CredentialBrokerSessionlessOrganisationTest pins the full contract (match admits, mismatch/null/empty deny, session ignores a wrong/matching assertion in both directions). Mutation-verified: reverting the sessionless admit or dropping the === org match makes a test fail. Also fixes a pre-existing bootstrap gap — DoriathStubs.php (fixtures) was never required by bootstrap-unit-local.php, so DoriathCredentialStoreTest/CredentialStoreResolverTest errored; now the full tests/Unit/Service/Credential/ dir is green (105 tests). Follow-up (not in this change): openconnector's BrokeredCallService must pass actingOrganisationId when resolving org-scoped credentials sessionlessly.
…dation methods Gate 6 (orphan-auth) un-blinding (openregister#444) reports 14 orphan validation methods on origin/development. Triaged each; 4 are superseded by a proven live enforcement point and are removed (with their dead tests), 10 are non-auth value-object predicates / deferred opt-in validators left in place and documented. No method was a live unprotected action — every security-relevant orphan already had a fail-closed live check. Deleted (zero lib/src callers, superseding live check verified): - AuditHandler::validateObjectOwnership (+ private helpers) — LogService::getLogs re-validates register/schema inline behind requireAdmin() - DestructionService::validateDestructionList — DestructionExecutionJob re-checks legal holds fail-closed at execution - SearchQueryHandler::isSearchTrailsEnabled — getEffectiveRecordingMode supersedes - ValidationHandler::validateSchemaObjects — validateAndSaveObjectsBySchema is the live /api/objects/validate path tests/Unit unchanged: 14763 tests, 27 err/17 fail/23 skip (identical before/after).
Sync the orphan-auth-remediation delta into openspec/specs/ (status: done) and move the change to changes/archive/2026-07-16-orphan-auth-remediation/.
…453) from wip/orphan-auth-remediation-archive into development
…ontext (_rbac:false) CredentialBrokerService::mint()'s contract is explicit — "minting is not itself a guarded operation; authorization is the caller's" (the controller runs the provider-catalogue + organisation-admin gate before calling; a repair-step migration is itself system-trusted). But its create saveObject() did NOT pass _rbac:false, so the write was re-gated by RBAC and ran as whatever principal the caller had. A SESSIONLESS caller — an occ/repair migration folding an inline source secret into the broker (openconnector#151) — is the anonymous principal, which OR denies from creating objects, so mint() threw NotAuthorizedException and the migration could never complete. The controller path only worked because it happened to carry an authorized admin session. This also made mint() inconsistent with itself: its rollback delete already passes _rbac:false. Add _rbac:false + _multitenancy:false to the create so mint honours its own contract for every caller, sessionless included. Live-verified on an isolated NC34 instance: before this change the migration's mint failed with NotAuthorizedException (secret correctly left intact, Phase D gate stayed closed); after it, mint→verify→null completes end-to-end (migrated:1, the source's inline apikey nulled, a credentialRef written, and the sessionless resolveInjectable round-trip via openregister#450 matched). The existing mint tests stubbed saveObject() ignoring its arguments, which is why the gap shipped green — the added test asserts the _rbac:false argument, not the return value.
…ject in system context (_rbac:false)' (#454) from wip/mint-sessionless-rbac-bypass into development
…ntainer (fleet-wide silent-empty bug) (#455)
…nt-less tools
A strict MCP client rejects the entire tools/list when any tool's
inputSchema.properties serialises as a JSON array. An argument-less tool
has an empty properties map, and PHP json_encode renders an empty PHP
array as [] (a JSON array) rather than {} (an object). The Claude CLI's
MCP client fails the whole handshake with:
Invalid input: expected record, received array
(at tools.N.inputSchema.properties)
so a single no-parameter tool breaks EVERY tool the server exposes — the
OpenBuild virtual-app builder tools (create schema/page/widget/menu)
included. Verified live: 'claude mcp add --transport http ... /api/mcp'
reported '! Connected - tools fetch failed' before, '✔ Connected' after.
Fix centrally in McpToolsService::listTools() (every provider's descriptors
flow through it): force an empty/absent inputSchema.properties to a stdClass
so it serialises as {}, and default an absent type to 'object'. Non-empty
properties maps are untouched.
Same json_encode([]) trap fixed for the Anthropic provider in hermiq#93.
MWest2020
requested review from
Rem-Dam,
SudoThijn,
WilcoLouwerse,
bbrands02,
remko48,
rjzondervan and
rubenvdlinde
as code owners
July 17, 2026 08:12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Heropend na de migratie van Codeberg naar GitHub (2026-07-14).
Origineel, met review-historie: https://codeberg.org/Conduction/openregister/pulls/456