Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/main/php/io/modelcontextprotocol/McpServer.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
},
Expand Down
42 changes: 25 additions & 17 deletions src/main/php/io/modelcontextprotocol/server/Delegate.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -59,13 +61,15 @@ protected function toolsIn(Type $type, string $namespace): iterable {
'properties' => $properties ?: (object)[],
'required' => $required,
],
];
] + (($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();
Expand All @@ -91,31 +95,35 @@ protected function promptsIn(Type $type, string $namespace): iterable {
'name' => $namespace.'_'.$name,
'description' => $method->comment() ?? ucfirst($name).' '.$namespace,
'arguments' => $arguments,
];
] + (($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,
];
}
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,
];
}
}

/** 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 */
Expand Down
11 changes: 7 additions & 4 deletions src/main/php/io/modelcontextprotocol/server/Resource.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,24 @@

class Resource {
public $uri, $mimeType, $dynamic;
public $template, $matches, $meta;
public $template, $matches, $struct;

/**
* Creates a new resource annotation
*
* @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(
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -89,6 +98,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)]
Expand Down Expand Up @@ -124,6 +140,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' => '<html>...</html>',
'_meta' => ['ui' => ['prefersBorder' => true]],
]],
$this->fixture()->readable('ui://greeting/card')([], new Request(new TestInput('GET', '/')))
);
}

#[Test]
public function read_resource_template() {
Assert::equals(
Expand Down
12 changes: 12 additions & 0 deletions src/test/php/io/modelcontextprotocol/unittest/Greetings.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ public function get($name) {
return "Hello {$name}";
}

/** Greeting card */
#[Resource('ui://greeting/card', 'text/html;profile=mcp-app')]
public function card() {
return ['text' => '<html>...</html>', '_meta' => ['ui' => ['prefersBorder' => true]]];
}

/** Greets users */
#[Prompt]
public function user(
Expand All @@ -40,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(
Expand Down
Loading