From 36032b72f771eef707b24bfbc82291b604121ea5 Mon Sep 17 00:00:00 2001 From: Torben Dannhauer Date: Wed, 15 Jul 2026 17:42:49 +0200 Subject: [PATCH] fix(core): preserve descriptor schemas in ApiRegistry re-wrapping The inputSchema and outputSchema fields of MethodDescriptor are the machine-readable contract for API discovery: JSON-RPC discovery and the MCP tools/list handler hand them to clients so callers know how to invoke a method. AI/MCP clients in particular depend on explicitly authored JSON schemas (enums, nested objects, per-property descriptions) to make correct tool calls; the auto-generated fallback built from the flat parameters array cannot express any of that. ApiRegistry undermined this: when getMethodDescriptor() and listMethods() re-wrap a provider-local descriptor to prefix the interface name ("edit" -> "wiki.edit"), they rebuilt the descriptor from only name, description, parameters, returnType and permissions. Explicitly authored schemas were silently discarded, so every discovery consumer saw degraded or empty schemas regardless of how carefully a provider declared them. Route both re-wrap sites through a shared prefixDescriptor() helper that copies every field, and add a regression test covering both getMethodDescriptor() and listMethods(). --- src/Api/ApiRegistry.php | 34 +++++++++++++++++------------ test/Unit/Api/ApiRegistryTest.php | 36 +++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/Api/ApiRegistry.php b/src/Api/ApiRegistry.php index e815eb93..806847e3 100644 --- a/src/Api/ApiRegistry.php +++ b/src/Api/ApiRegistry.php @@ -71,13 +71,7 @@ public function getMethodDescriptor(string $method, ?ApiCallContext $context = n return null; } - return new MethodDescriptor( - name: $interface . '.' . $descriptor->name, - description: $descriptor->description, - parameters: $descriptor->parameters, - returnType: $descriptor->returnType, - permissions: $descriptor->permissions, - ); + return $this->prefixDescriptor($interface, $descriptor); } /** @@ -88,13 +82,7 @@ public function listMethods(?ApiCallContext $context = null): array $all = []; foreach ($this->providers as $interface => $provider) { foreach ($provider->listMethods($context) as $descriptor) { - $all[] = new MethodDescriptor( - name: $interface . '.' . $descriptor->name, - description: $descriptor->description, - parameters: $descriptor->parameters, - returnType: $descriptor->returnType, - permissions: $descriptor->permissions, - ); + $all[] = $this->prefixDescriptor($interface, $descriptor); } } @@ -153,6 +141,24 @@ public function getProviderForInterface(string $interface): ApiProvider|MethodIn return $this->providers[$interface] ?? null; } + /** + * Re-wrap a provider-local descriptor with the interface-prefixed name, + * preserving all other metadata (including the JSON schemas needed by + * MCP tool listings). + */ + private function prefixDescriptor(string $interface, MethodDescriptor $descriptor): MethodDescriptor + { + return new MethodDescriptor( + name: $interface . '.' . $descriptor->name, + description: $descriptor->description, + parameters: $descriptor->parameters, + returnType: $descriptor->returnType, + inputSchema: $descriptor->inputSchema, + outputSchema: $descriptor->outputSchema, + permissions: $descriptor->permissions, + ); + } + /** * @return array{string, string}|null */ diff --git a/test/Unit/Api/ApiRegistryTest.php b/test/Unit/Api/ApiRegistryTest.php index c932b097..dc782a4b 100644 --- a/test/Unit/Api/ApiRegistryTest.php +++ b/test/Unit/Api/ApiRegistryTest.php @@ -85,6 +85,42 @@ public function testGetMethodDescriptorPrefixed(): void $this->assertSame('List items', $desc->description); } + public function testDescriptorSchemasPreserved(): void + { + $inputSchema = [ + 'type' => 'object', + 'properties' => [ + 'pagename' => ['type' => 'string', 'description' => 'Page to edit'], + ], + 'required' => ['pagename'], + ]; + $outputSchema = ['type' => 'object']; + $provider = $this->makeProvider( + ['edit' => fn() => null], + ['edit' => new MethodDescriptor( + 'edit', + description: 'Edit a page', + inputSchema: $inputSchema, + outputSchema: $outputSchema, + )], + ); + $registry = new ApiRegistry(); + $registry->registerProvider('wiki', $provider); + + $desc = $registry->getMethodDescriptor('wiki.edit'); + + $this->assertNotNull($desc); + $this->assertSame($inputSchema, $desc->inputSchema); + $this->assertSame($outputSchema, $desc->outputSchema); + + $listed = $registry->listMethods(); + + $this->assertCount(1, $listed); + $this->assertSame('wiki.edit', $listed[0]->name); + $this->assertSame($inputSchema, $listed[0]->inputSchema); + $this->assertSame($outputSchema, $listed[0]->outputSchema); + } + public function testUnknownInterfaceHasMethodReturnsFalse(): void { $registry = new ApiRegistry();