Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.

Commit 7bb5ab8

Browse files
Merge pull request #15 from EdouardCourty/feat/support-resources
feat(mcp): add support for resources
2 parents a334315 + c0aa104 commit 7bb5ab8

39 files changed

Lines changed: 1760 additions & 3 deletions

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
### Added
11+
12+
- Added support for Resources
13+
- Support for `resources/list`, `resources/templates/list` and `resources/read` JSON-RPC methods
14+
- Added `AsResource` attribute to register resources
15+
- Added events for resource management (`ResourceReadEvent`, `ResourceReadResultEvent`)
16+
- Added tests to cover the resource functionality
17+
18+
### Updated
19+
20+
- Updated MCP Protocol to version `2025-06-18`
21+
- Updated the configuration to include the `title` value, used in the initialization phase (JSON-RPC `initialize` method)
22+
- Updated the `initialize` JSON-RPC method to return the `title` value from the configuration
23+
- Updated the README to include the new resource functionality and examples
24+
825
## [1.1.0] - 2025-06-19
926

1027
### Added

README.md

Lines changed: 234 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,20 @@ _Read the [official MCP specification](https://modelcontextprotocol.io/docs/conc
2121
- [Tool Events](#tool-events)
2222
- [Input Schema Management](#input-schema-management)
2323
- [JSON-RPC Integration](#json-rpc-integration)
24+
- [Resources](#resources)
25+
- [Creating Resources](#creating-resources)
26+
- [Static Resources](#static-resources)
27+
- [Templated Resources](#templated-resources)
28+
- [Multiple Parameters](#multiple-parameters)
29+
- [Parameter Type Casting](#parameter-type-casting)
30+
- [Resource Results](#resource-results)
31+
- [Resource Events](#resource-events)
32+
- [JSON-RPC Integration](#json-rpc-integration-1)
2433
- [Prompts](#prompts)
2534
- [Creating Prompts](#creating-prompts)
2635
- [Prompt Results](#prompt-results)
2736
- [Prompt Events](#prompt-events)
28-
- [JSON-RPC Integration](#json-rpc-integration-1)
37+
- [JSON-RPC Integration](#json-rpc-integration-2)
2938
- [JSON-RPC Methods](#json-rpc-methods)
3039
- [Built-in Methods](#built-in-methods)
3140
- [Custom Methods](#custom-methods)
@@ -273,6 +282,228 @@ This ensures that your tool handlers always receive properly validated and sanit
273282
- **`tools/list`**: Lists all available tools and their definitions.
274283
- **`tools/call`**: Executes a tool by name, with the provided input data.
275284

285+
## Resources
286+
287+
Resources are data sources that can be accessed by clients via their URI.
288+
They can represent files, database records, or any other data that can be identified by a URI.
289+
290+
### Creating Resources
291+
292+
1. Create a new class that will handle your resource logic
293+
2. Use the `#[AsResource]` attribute to register your resource
294+
3. Define the URI pattern for your resource (static or templated)
295+
4. Implement the `__invoke` method to handle the resource logic and return a `ResourceResult`
296+
297+
_As Resource classes are services within the Symfony application, any dependency can be injected in it, using the constructor, like any other service._
298+
299+
#### Static Resources
300+
301+
Static resources have a fixed URI that doesn't change. They are useful for resources that don't require parameters.
302+
303+
Example:
304+
```php
305+
<?php
306+
307+
use Ecourty\McpServerBundle\Attribute\AsResource;
308+
use Ecourty\McpServerBundle\IO\Resource\ResourceResult;
309+
use Ecourty\McpServerBundle\IO\Resource\TextResource;
310+
311+
#[AsResource(
312+
uri: 'file://robots.txt',
313+
name: 'robots_txt',
314+
title: 'Get the Robots.txt file',
315+
description: 'This resource returns the content of the robots.txt file.',
316+
mimeType: 'text/plain',
317+
)]
318+
class RobotsFileResource
319+
{
320+
private const string FILE_PATH = __DIR__ . '/../Resources/robots.txt';
321+
322+
public function __invoke(): ResourceResult
323+
{
324+
$fileContent = (string) file_get_contents(self::FILE_PATH);
325+
$encodedFileContent = base64_encode($fileContent);
326+
327+
return new ResourceResult([
328+
new BinaryResource('file://robots.txt', 'text/plain', $encodedFileContent),
329+
]);
330+
}
331+
}
332+
```
333+
334+
#### Templated Resources
335+
336+
Templated resources use URI templates with parameters enclosed in curly braces (e.g., `{id}`). These parameters are automatically extracted from the URI and passed to the `__invoke` method as arguments.
337+
338+
Example:
339+
```php
340+
<?php
341+
342+
use Ecourty\McpServerBundle\Attribute\AsResource;
343+
use Ecourty\McpServerBundle\IO\Resource\ResourceResult;
344+
use Ecourty\McpServerBundle\IO\Resource\TextResource;
345+
346+
#[AsResource(
347+
uri: 'database://user/{id}',
348+
name: 'user_data',
349+
title: 'Get User Data',
350+
description: 'Gathers the data of a user by their ID.',
351+
mimeType: 'application/json',
352+
)]
353+
class UserResource
354+
{
355+
public function __construct(
356+
private readonly EntityManagerInterface $entityManager,
357+
private readonly SerializerInterface $serializer,
358+
) {
359+
}
360+
361+
public function __invoke(int $id): ResourceResult
362+
{
363+
$user = $this->entityManager->find(User::class, $id);
364+
if ($user === null) {
365+
throw new \RuntimeException('User not found');
366+
}
367+
368+
$stringifiedUserData = $this->serializer->serialize($user, 'json');
369+
370+
return new ResourceResult([
371+
new TextResource(
372+
uri: 'database://user/' . $id,
373+
mimeType: 'application/json',
374+
text: $stringifiedUserData,
375+
),
376+
]);
377+
}
378+
}
379+
```
380+
381+
In this example:
382+
- The URI template `database://user/{id}` defines a parameter named `id`
383+
- When a client requests `database://user/123`, the parameter `123` is extracted
384+
- The `__invoke` method receives `123` as an `int` parameter (automatic type casting is performed)
385+
- The resource returns user data for ID 123
386+
387+
#### Multiple Parameters
388+
389+
You can define multiple parameters in a single URI template:
390+
391+
```php
392+
#[AsResource(
393+
uri: 'api://users/{userId}/posts/{postId}',
394+
name: 'user_post',
395+
title: 'Get User Post',
396+
description: 'Retrieves a specific post by a user.',
397+
mimeType: 'application/json',
398+
)]
399+
class UserPostResource
400+
{
401+
public function __invoke(int $userId, int $postId): ResourceResult
402+
{
403+
// Your logic here...
404+
$post = $this->postRepository->findByUserAndPost($userId, $postId);
405+
406+
return new ResourceResult([
407+
new TextResource(
408+
uri: "api://users/{$userId}/posts/{$postId}",
409+
mimeType: 'application/json',
410+
text: json_encode($post),
411+
),
412+
]);
413+
}
414+
}
415+
```
416+
417+
#### Parameter Type Casting
418+
419+
The bundle automatically casts URI parameters to the appropriate types based on the method signature:
420+
421+
- `int` parameters are cast to integers
422+
- `float` parameters are cast to floats
423+
- `bool` parameters are cast to booleans
424+
- `string` parameters remain as strings
425+
- `array` parameters are JSON-decoded into arrays using `json_decode` if they are JSON strings
426+
427+
### Resource Results
428+
429+
The [MCP specification](https://modelcontextprotocol.io/specification/2025-06-18/server/resources#reading-resources) states that resource results should consist of an array of resource objects.
430+
The bundle provides several result types that can be combined in a single `ResourceResult` object:
431+
432+
- `TextResource`: For text-based content (JSON, XML, plain text, etc.)
433+
- `BinaryResource`: For binary content (images, audio, video, files, etc.), should be base-64 encoded
434+
435+
All resource results must be wrapped in a `ResourceResult` object, which can contain multiple resources.
436+
437+
Example:
438+
```php
439+
<?php
440+
441+
use Ecourty\McpServerBundle\IO\Resource\ResourceResult;
442+
use Ecourty\McpServerBundle\IO\Resource\TextResource;
443+
use Ecourty\McpServerBundle\IO\Resource\BinaryResource;
444+
445+
#[AsResource(
446+
// ...
447+
)]
448+
class MyResource
449+
{
450+
public function __invoke(): ResourceResult
451+
{
452+
$jsonData = json_encode(['status' => 'success']);
453+
$imageData = base64_encode(file_get_contents('image.jpg'));
454+
455+
return new ResourceResult([
456+
new TextResource(
457+
uri: 'api://data/status',
458+
mimeType: 'application/json',
459+
text: $jsonData,
460+
),
461+
new BinaryResource(
462+
uri: 'file://image.jpg',
463+
mimeType: 'image/jpeg',
464+
blob: $imageData,
465+
),
466+
]);
467+
}
468+
}
469+
```
470+
471+
The `ResourceResult` class provides the following features:
472+
- Combine multiple resources of different types
473+
- Automatic serialization to the correct format
474+
- Type safety for all resources
475+
476+
### Resource Events
477+
478+
The bundle provides several events that you can listen to:
479+
480+
- `ResourceReadEvent`: Dispatched before a resource is read, contains the URI
481+
- `ResourceReadResultEvent`: Dispatched after a resource has been read, contains the URI and results
482+
483+
Example of event listener:
484+
```php
485+
<?php
486+
487+
use Ecourty\McpServerBundle\Event\ResourceReadEvent;
488+
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
489+
490+
#[AsEventListener(event: ResourceReadEvent::class)]
491+
class ResourceReadListener
492+
{
493+
public function __invoke(ResourceReadEvent $event): void
494+
{
495+
// Your logic here...
496+
// Log the resource access, add caching, etc.
497+
}
498+
}
499+
```
500+
501+
### JSON-RPC Integration
502+
503+
- **`resources/list`**: Lists all available direct (static) resources and their definitions.
504+
- **`resources/templates/list`**: Lists all available templated resources and their definitions.
505+
- **`resources/read`**: Retrieves a resource by its URI, automatically matching templated resources and extracting parameters.
506+
276507
## Prompts
277508

278509
Prompts are reusable templates that can be dynamically generated and returned by the MCP server.
@@ -526,3 +757,5 @@ composer test
526757
## License
527758

528759
This bundle is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
760+
761+

src/Attribute/AsResource.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Attribute;
6+
7+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
8+
class AsResource
9+
{
10+
public function __construct(
11+
public readonly string $uri, // Unique identifier for the resource
12+
public readonly string $name, // The name of the resource
13+
public readonly ?string $title = null, // Optional human-readable title
14+
public readonly ?string $description = null, // Optional description
15+
public readonly ?string $mimeType = null, // Optional MIME type of the resource
16+
public readonly ?string $size = null, // Optional size of the resource, in bytes
17+
) {
18+
}
19+
20+
public function isTemplatedResource(): bool
21+
{
22+
return str_contains($this->uri, '{') && str_contains($this->uri, '}');
23+
}
24+
}

0 commit comments

Comments
 (0)