From 021ca655bce1c0e4aa50823cf94d527bc4b04804 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Feb 2026 20:41:00 +0100 Subject: [PATCH 1/3] Make it possible to supply meta data for discovery and resources --- .../modelcontextprotocol/McpServer.class.php | 3 +-- .../server/Delegate.class.php | 21 +++++++++++-------- .../unittest/DelegateTest.class.php | 20 ++++++++++++++++++ .../unittest/Greetings.class.php | 8 ++++++- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/main/php/io/modelcontextprotocol/McpServer.class.php b/src/main/php/io/modelcontextprotocol/McpServer.class.php index 76ba6e3..94d84ff 100755 --- a/src/main/php/io/modelcontextprotocol/McpServer.class.php +++ b/src/main/php/io/modelcontextprotocol/McpServer.class.php @@ -67,8 +67,7 @@ public function __construct($delegate, string $version= '2025-06-18') { }, 'resources/read' => function($payload, $request) { if ($readable= $this->delegate->readable($payload['params']['uri'])) { - $contents= $readable([], $request); - return ['contents' => $contents]; + return ['contents' => $readable([], $request)]; } throw new NoSuchElementException($payload['params']['uri']); }, diff --git a/src/main/php/io/modelcontextprotocol/server/Delegate.class.php b/src/main/php/io/modelcontextprotocol/server/Delegate.class.php index eec7cdd..55b975f 100755 --- a/src/main/php/io/modelcontextprotocol/server/Delegate.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Delegate.class.php @@ -59,7 +59,7 @@ protected function toolsIn(Type $type, string $namespace): iterable { 'properties' => $properties ?: (object)[], 'required' => $required, ], - ]; + ] + (($meta= $method->annotation(Meta::class)) ? ['_meta' => $meta->argument(0)] : []); } } @@ -91,7 +91,7 @@ protected function promptsIn(Type $type, string $namespace): iterable { 'name' => $namespace.'_'.$name, 'description' => $method->comment() ?? ucfirst($name).' '.$namespace, 'arguments' => $arguments, - ]; + ] + (($meta= $method->annotation(Meta::class)) ? ['_meta' => $meta->argument(0)] : []); } } @@ -103,19 +103,22 @@ protected function resourcesIn(Type $type, string $namespace, bool $templates): $templates === $resource->template && yield $resource->meta + [ 'name' => $namespace.'_'.$name, 'description' => $method->comment() ?? null, - ]; + ] + (($meta= $method->annotation(Meta::class)) ? ['_meta' => $meta->argument(0)] : []); } } } /** Returns contents of a given resource */ protected function contentsOf(string $uri, string $mimeType, $result) { - return [ - ['uri' => $uri, 'mimeType' => $mimeType] + ($result instanceof Bytes - ? ['blob' => base64_encode($result)] - : ['text' => $result] - ) - ]; + $content= ['uri' => $uri, 'mimeType' => $mimeType]; + if (is_array($result)) { + $content+= $result; + } else if ($result instanceof Bytes) { + $content['blob']= base64_encode($result); + } else { + $content['text']= (string)$result; + } + return [$content]; } /** Access a given method */ diff --git a/src/test/php/io/modelcontextprotocol/unittest/DelegateTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/DelegateTest.class.php index 0776327..02f1b28 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/DelegateTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/DelegateTest.class.php @@ -89,6 +89,13 @@ public function resources() { 'description' => 'Greeting icon', 'mimeType' => 'image/gif', 'dynamic' => true, + ], + [ + 'uri' => 'ui://greeting/card', + 'name' => 'greetings_card', + 'description' => 'Greeting card', + 'mimeType' => 'text/html;profile=mcp-app', + 'dynamic' => false, ] ], [...$this->fixture()->resources(false)] @@ -124,6 +131,19 @@ public function read_binary_resource() { ); } + #[Test] + public function read_app_resource() { + Assert::equals( + [[ + 'uri' => 'ui://greeting/card', + 'mimeType' => 'text/html;profile=mcp-app', + 'text' => '...', + '_meta' => ['ui' => ['prefersBorder' => true]], + ]], + $this->fixture()->readable('ui://greeting/card')([], new Request(new TestInput('GET', '/'))) + ); + } + #[Test] public function read_resource_template() { Assert::equals( diff --git a/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php b/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php index cbe1297..de2c2de 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php @@ -1,6 +1,6 @@ '...', '_meta' => ['ui' => ['prefersBorder' => true]]]; + } + /** Greets users */ #[Prompt] public function user( From dc68288ace5cbb9d8a33f53b25729530f1654091 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Mon, 2 Feb 2026 20:49:57 +0100 Subject: [PATCH 2/3] Add test for `Meta` annotation --- .../modelcontextprotocol/unittest/DelegateTest.class.php | 9 +++++++++ .../io/modelcontextprotocol/unittest/Greetings.class.php | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/src/test/php/io/modelcontextprotocol/unittest/DelegateTest.class.php b/src/test/php/io/modelcontextprotocol/unittest/DelegateTest.class.php index 02f1b28..fc88ac2 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/DelegateTest.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/DelegateTest.class.php @@ -20,6 +20,15 @@ public function tools() { 'properties' => (object)[], 'required' => [], ] + ], [ + 'name' => 'greetings_launch', + 'description' => 'Launches greeting card designer', + 'inputSchema' => [ + 'type' => 'object', + 'properties' => (object)[], + 'required' => [], + ], + '_meta' => ['ui' => ['resourceUri' => 'ui://greeting/card']], ], [ 'name' => 'greetings_repeat', 'description' => 'Repeats a given greeting', diff --git a/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php b/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php index de2c2de..fb9b7f4 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php @@ -46,6 +46,12 @@ public function languages() { return ['en', 'de']; } + /** Launches greeting card designer */ + #[Tool, Meta(['ui' => ['resourceUri' => 'ui://greeting/card']])] + public function launch() { + return 'App launching...'; + } + /** Repeats a given greeting */ #[Tool] public function repeat( From 4532f8c62baef21a04f518bc28498e084b1f6e3e Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Tue, 3 Feb 2026 20:18:59 +0100 Subject: [PATCH 3/3] Use meta annotation argument instead of standalone annotation See https://github.com/xp-forge/mcp/pull/20#issuecomment-3842989274 --- .../server/Delegate.class.php | 27 +++++++++++-------- .../server/Resource.class.php | 11 +++++--- .../unittest/Greetings.class.php | 4 +-- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/main/php/io/modelcontextprotocol/server/Delegate.class.php b/src/main/php/io/modelcontextprotocol/server/Delegate.class.php index 55b975f..eefd66d 100755 --- a/src/main/php/io/modelcontextprotocol/server/Delegate.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Delegate.class.php @@ -37,7 +37,9 @@ public abstract function invokeable($tool); /** Yields all tools in a given type */ protected function toolsIn(Type $type, string $namespace): iterable { - foreach ($type->methods()->annotated(Tool::class) as $name => $method) { + foreach ($type->methods() as $name => $method) { + if (null === ($annotation= $method->annotation(Tool::class))) continue; + $properties= $required= []; foreach ($method->parameters() as $param => $reflect) { $annotations= $reflect->annotations(); @@ -59,13 +61,15 @@ protected function toolsIn(Type $type, string $namespace): iterable { 'properties' => $properties ?: (object)[], 'required' => $required, ], - ] + (($meta= $method->annotation(Meta::class)) ? ['_meta' => $meta->argument(0)] : []); + ] + (($meta= $annotation->argument('meta')) ? ['_meta' => $meta] : []); } } /** Yields all prompts in a given type */ protected function promptsIn(Type $type, string $namespace): iterable { - foreach ($type->methods()->annotated(Prompt::class) as $name => $method) { + foreach ($type->methods() as $name => $method) { + if (null === ($annotation= $method->annotation(Prompt::class))) continue; + $arguments= []; foreach ($method->parameters() as $param => $reflect) { $annotations= $reflect->annotations(); @@ -91,20 +95,21 @@ protected function promptsIn(Type $type, string $namespace): iterable { 'name' => $namespace.'_'.$name, 'description' => $method->comment() ?? ucfirst($name).' '.$namespace, 'arguments' => $arguments, - ] + (($meta= $method->annotation(Meta::class)) ? ['_meta' => $meta->argument(0)] : []); + ] + (($meta= $annotation->argument('meta')) ? ['_meta' => $meta] : []); } } /** Yields all resources in a given type */ protected function resourcesIn(Type $type, string $namespace, bool $templates): iterable { foreach ($type->methods() as $name => $method) { - if ($annotation= $method->annotation(Resource::class)) { - $resource= $annotation->newInstance(); - $templates === $resource->template && yield $resource->meta + [ - 'name' => $namespace.'_'.$name, - 'description' => $method->comment() ?? null, - ] + (($meta= $method->annotation(Meta::class)) ? ['_meta' => $meta->argument(0)] : []); - } + if (null === ($annotation= $method->annotation(Resource::class))) continue; + + $resource= $annotation->newInstance(); + $templates === $resource->template && yield $resource->struct + [ + 'mimeType' => $resource->mimeType, + 'name' => $namespace.'_'.$name, + 'description' => $method->comment() ?? null, + ]; } } diff --git a/src/main/php/io/modelcontextprotocol/server/Resource.class.php b/src/main/php/io/modelcontextprotocol/server/Resource.class.php index 692ed74..123a433 100755 --- a/src/main/php/io/modelcontextprotocol/server/Resource.class.php +++ b/src/main/php/io/modelcontextprotocol/server/Resource.class.php @@ -2,7 +2,7 @@ class Resource { public $uri, $mimeType, $dynamic; - public $template, $matches, $meta; + public $template, $matches, $struct; /** * Creates a new resource annotation @@ -10,15 +10,16 @@ class Resource { * @param string $uri * @param string $mimeType * @param bool $dynamic + * @param [:var] $meta */ - public function __construct($uri, $mimeType= 'text/plain', $dynamic= false) { + public function __construct($uri, $mimeType= 'text/plain', $dynamic= false, $meta= []) { $this->uri= $uri; $this->mimeType= $mimeType; $this->dynamic= $dynamic; if (false === strpos($uri, '{')) { $this->template= false; - $this->meta= ['uri' => $uri, 'mimeType' => $mimeType, 'dynamic' => $dynamic]; + $this->struct= ['uri' => $uri, 'dynamic' => $dynamic]; $this->matches= fn($compare) => $compare === $uri ? (object)[] : null; } else { $pattern= '#^'.preg_replace( @@ -28,11 +29,13 @@ public function __construct($uri, $mimeType= 'text/plain', $dynamic= false) { ).'#'; $this->template= true; - $this->meta= ['uriTemplate' => $uri, 'mimeType' => $mimeType]; + $this->struct= ['uriTemplate' => $uri]; $this->matches= fn($compare) => preg_match($pattern, $compare, $matches) ? array_filter($matches, fn($key) => is_string($key), ARRAY_FILTER_USE_KEY) : null ; } + + $meta && $this->struct['_meta']= $meta; } } \ No newline at end of file diff --git a/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php b/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php index fb9b7f4..083ec07 100755 --- a/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php +++ b/src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php @@ -1,6 +1,6 @@ ['resourceUri' => 'ui://greeting/card']])] + #[Tool(meta: ['ui' => ['resourceUri' => 'ui://greeting/card']])] public function launch() { return 'App launching...'; }