Skip to content

Commit 8f4c0ce

Browse files
authored
Add new read functionality for resource tempaltes (#35)
1 parent a41c007 commit 8f4c0ce

File tree

15 files changed

+854
-53
lines changed

15 files changed

+854
-53
lines changed

CLAUDE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ php artisan octane:start
5252
- **InitializeHandler**: Handles client-server handshake and capability negotiation
5353
- **ToolsListHandler**: Returns available MCP tools to clients
5454
- **ToolsCallHandler**: Executes specific tool calls with parameters
55+
- **ResourcesListHandler**: Returns available resources (static + template-generated)
56+
- **ResourcesTemplatesListHandler**: Returns resource template definitions
57+
- **ResourcesReadHandler**: Reads resource content by URI
5558
- **PingHandler**: Health check endpoint
5659

5760
### Tool System
@@ -60,6 +63,16 @@ Tools implement `ToolInterface` and are registered in `config/mcp-server.php`. E
6063
- Execution logic
6164
- Output formatting
6265

66+
### Resource System
67+
Resources expose data to LLMs and are registered in `config/mcp-server.php`. Two types:
68+
- **Static Resources**: Concrete resources with fixed URIs
69+
- **Resource Templates**: Dynamic resources using URI templates (RFC 6570)
70+
71+
Resource Templates support:
72+
- URI pattern matching with variables (e.g., `database://users/{id}`)
73+
- Optional `list()` method for dynamic resource discovery
74+
- Parameter extraction for `read()` method implementation
75+
6376
### Configuration
6477
Primary config: `config/mcp-server.php`
6578
- Server info (name, version)
@@ -81,6 +94,14 @@ Primary config: `config/mcp-server.php`
8194
- Example tools: `src/Services/ToolService/Examples/`
8295
- Tool stub template: `src/stubs/tool.stub`
8396

97+
### Key Files for Resource Development
98+
- Resource base class: `src/Services/ResourceService/Resource.php`
99+
- ResourceTemplate base class: `src/Services/ResourceService/ResourceTemplate.php`
100+
- Resource repository: `src/Services/ResourceService/ResourceRepository.php`
101+
- URI template utility: `src/Utils/UriTemplateUtil.php`
102+
- Example resources: `src/Services/ResourceService/Examples/`
103+
- Resource stub templates: `src/stubs/resource.stub`, `src/stubs/resource_template.stub`
104+
84105
## Package Development Notes
85106

86107
### Project Structure

README.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -547,20 +547,40 @@ patterns clients can use. A resource is identified by a URI such as
547547
`file:///logs/app.log` and may optionally define metadata like `mimeType` or
548548
`size`.
549549

550+
**Resource Templates with Dynamic Listing**: Templates can optionally implement a `list()` method to provide concrete resource instances that match the template pattern. This allows clients to discover available resources dynamically. The `list()` method enables ResourceTemplate instances to generate a list of specific resources that can be read through the template's `read()` method.
551+
550552
List available resources using the `resources/list` endpoint and read their
551-
contents with `resources/read`. The `resources/list` endpoint returns both
552-
concrete resources and resource templates in a single response:
553+
contents with `resources/read`. The `resources/list` endpoint returns an array
554+
of concrete resources, including both static resources and dynamically generated
555+
resources from templates that implement the `list()` method:
553556

554557
```json
555558
{
556-
"resources": [...], // Array of concrete resources
557-
"resourceTemplates": [...] // Array of URI templates
559+
"resources": [
560+
{
561+
"uri": "file:///logs/app.log",
562+
"name": "Application Log",
563+
"mimeType": "text/plain"
564+
},
565+
{
566+
"uri": "database://users/123",
567+
"name": "User: John Doe",
568+
"description": "Profile data for John Doe",
569+
"mimeType": "application/json"
570+
}
571+
]
558572
}
559573
```
560574

561-
Resource templates allow clients to construct dynamic resource identifiers
562-
using URI templates (RFC 6570). You can also list templates separately using
563-
the `resources/templates/list` endpoint:
575+
**Dynamic Resource Reading**: Resource templates support URI template patterns (RFC 6570) that allow clients to construct dynamic resource identifiers. When a client requests a resource URI that matches a template pattern, the template's `read()` method is called with extracted parameters to generate the resource content.
576+
577+
Example workflow:
578+
1. Template defines pattern: `"database://users/{userId}/profile"`
579+
2. Client requests: `"database://users/123/profile"`
580+
3. Template extracts `{userId: "123"}` and calls `read()` method
581+
4. Template returns user profile data for user ID 123
582+
583+
You can also list templates separately using the `resources/templates/list` endpoint:
564584

565585
```bash
566586
# List only resource templates

config/mcp-server.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@
317317
'resources' => [
318318
// Example resource - Remove in production
319319
\OPGG\LaravelMcpServer\Services\ResourceService\Examples\LogFileResource::class,
320+
\OPGG\LaravelMcpServer\Services\ResourceService\Examples\UserListResource::class,
320321

321322
// ===== REGISTER YOUR STATIC RESOURCES BELOW =====
322323
// Examples:
@@ -330,6 +331,7 @@
330331
'resource_templates' => [
331332
// Example template - Remove in production
332333
\OPGG\LaravelMcpServer\Services\ResourceService\Examples\LogFileTemplate::class,
334+
\OPGG\LaravelMcpServer\Services\ResourceService\Examples\UserResourceTemplate::class,
333335

334336
// ===== REGISTER YOUR RESOURCE TEMPLATES BELOW =====
335337
// Examples:

src/Server/Request/ResourcesListHandler.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ public function execute(string $method, ?array $params = null): array
2121
{
2222
return [
2323
'resources' => $this->repository->getResourceSchemas(),
24-
'resourceTemplates' => $this->repository->getTemplateSchemas(),
2524
];
2625
}
2726
}

src/Server/Request/ResourcesReadHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function execute(string $method, ?array $params = null): array
2626
throw new JsonRpcErrorException(message: 'uri is required', code: JsonRpcErrorCode::INVALID_REQUEST);
2727
}
2828

29-
$content = $this->repository->read($uri);
29+
$content = $this->repository->readResource($uri);
3030
if ($content === null) {
3131
throw new JsonRpcErrorException(message: 'Resource not found', code: JsonRpcErrorCode::INVALID_PARAMS);
3232
}

src/Services/ResourceService/Examples/LogFileTemplate.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,30 @@ class LogFileTemplate extends ResourceTemplate
1313
public ?string $description = 'Access log file for the given date';
1414

1515
public ?string $mimeType = 'text/plain';
16+
17+
/**
18+
* Read log file content for the specified date.
19+
*
20+
* @param string $uri The full URI being requested (e.g., "file:///logs/2024-01-01.log")
21+
* @param array $params Extracted parameters (e.g., ['date' => '2024-01-01'])
22+
* @return array Resource content with uri, mimeType, and text
23+
*/
24+
public function read(string $uri, array $params): array
25+
{
26+
$date = $params['date'] ?? 'unknown';
27+
28+
// In a real implementation, you would read the actual log file
29+
// For this example, we'll return mock log data
30+
$logContent = "Log entries for {$date}\n";
31+
$logContent .= "[{$date} 10:00:00] INFO: Application started\n";
32+
$logContent .= "[{$date} 10:05:00] INFO: Processing requests\n";
33+
$logContent .= "[{$date} 10:10:00] WARNING: High memory usage detected\n";
34+
$logContent .= "[{$date} 10:15:00] INFO: Memory usage normalized\n";
35+
36+
return [
37+
'uri' => $uri,
38+
'mimeType' => $this->mimeType,
39+
'text' => $logContent,
40+
];
41+
}
1642
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace OPGG\LaravelMcpServer\Services\ResourceService\Examples;
4+
5+
use OPGG\LaravelMcpServer\Services\ResourceService\Resource;
6+
7+
/**
8+
* Example static Resource that provides a list of all users.
9+
* This complements the UserResourceTemplate to show both static and dynamic resources.
10+
*
11+
* Usage:
12+
* - Static URI: "database://users"
13+
* - Returns a list of all users
14+
* - Users can then use the UserResourceTemplate to get individual user details
15+
*/
16+
class UserListResource extends Resource
17+
{
18+
public string $uri = 'database://users';
19+
20+
public string $name = 'Users List';
21+
22+
public ?string $description = 'List of all users in the database';
23+
24+
public ?string $mimeType = 'application/json';
25+
26+
/**
27+
* Read and return the list of all users.
28+
*
29+
* In a real implementation, this would query the database
30+
* to get all users and return a summary list.
31+
*/
32+
public function read(): array
33+
{
34+
// In a real implementation, you would query your database:
35+
// $users = User::select(['id', 'name', 'email'])->get();
36+
//
37+
// For this example, we'll return mock data:
38+
$users = [
39+
['id' => 1, 'name' => 'User 1', 'email' => '[email protected]'],
40+
['id' => 2, 'name' => 'User 2', 'email' => '[email protected]'],
41+
['id' => 3, 'name' => 'User 3', 'email' => '[email protected]'],
42+
];
43+
44+
$response = [
45+
'total' => count($users),
46+
'users' => $users,
47+
'template' => 'database://users/{id}',
48+
'description' => 'Use the template URI to get individual user details',
49+
];
50+
51+
return [
52+
'uri' => $this->uri,
53+
'mimeType' => $this->mimeType,
54+
'text' => json_encode($response, JSON_PRETTY_PRINT),
55+
];
56+
}
57+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
namespace OPGG\LaravelMcpServer\Services\ResourceService\Examples;
4+
5+
use OPGG\LaravelMcpServer\Services\ResourceService\ResourceTemplate;
6+
7+
/**
8+
* Example ResourceTemplate that demonstrates how to create dynamic user resources.
9+
* This solves the problem described in GitHub discussion #32.
10+
*
11+
* Usage:
12+
* - Template URI: "database://users/{id}"
13+
* - Client can request: "database://users/123" to get user with ID 123
14+
* - The read() method will be called with ['id' => '123'] as parameters
15+
*/
16+
class UserResourceTemplate extends ResourceTemplate
17+
{
18+
public string $uriTemplate = 'database://users/{id}';
19+
20+
public string $name = 'User by ID';
21+
22+
public ?string $description = 'Access individual user details by user ID';
23+
24+
public ?string $mimeType = 'application/json';
25+
26+
/**
27+
* List all available user resources.
28+
*
29+
* This method returns a list of concrete user resources that can be accessed
30+
* through this template. In a real implementation, you would query your database
31+
* to get all available users.
32+
*
33+
* @return array Array of user resource definitions
34+
*/
35+
public function list(): ?array
36+
{
37+
// In a real implementation, you would query your database:
38+
// $users = User::select(['id', 'name'])->get();
39+
//
40+
// For this example, we'll return mock data:
41+
$users = [
42+
['id' => 1, 'name' => 'Alice'],
43+
['id' => 2, 'name' => 'Bob'],
44+
['id' => 3, 'name' => 'Charlie'],
45+
];
46+
47+
$resources = [];
48+
foreach ($users as $user) {
49+
$resources[] = [
50+
'uri' => "database://users/{$user['id']}",
51+
'name' => "User: {$user['name']}",
52+
'description' => "Profile data for user {$user['name']} (ID: {$user['id']})",
53+
'mimeType' => $this->mimeType,
54+
];
55+
}
56+
57+
return $resources;
58+
}
59+
60+
/**
61+
* Read user data for the specified user ID.
62+
*
63+
* In a real implementation, this would:
64+
* 1. Extract the user ID from the parameters
65+
* 2. Query the database to fetch user details
66+
* 3. Return the user data as JSON
67+
*
68+
* @param string $uri The full URI being requested (e.g., "database://users/123")
69+
* @param array $params Extracted parameters (e.g., ['id' => '123'])
70+
* @return array Resource content with uri, mimeType, and text/blob
71+
*/
72+
public function read(string $uri, array $params): array
73+
{
74+
$userId = $params['id'] ?? null;
75+
76+
if ($userId === null) {
77+
return [
78+
'uri' => $uri,
79+
'mimeType' => 'application/json',
80+
'text' => json_encode(['error' => 'Missing user ID'], JSON_PRETTY_PRINT),
81+
];
82+
}
83+
84+
// In a real implementation, you would query your database here:
85+
// $user = User::find($userId);
86+
//
87+
// For this example, we'll return mock data:
88+
$userData = [
89+
'id' => (int) $userId,
90+
'name' => "User {$userId}",
91+
'email' => "user{$userId}@example.com",
92+
'created_at' => '2024-01-01T00:00:00Z',
93+
'profile' => [
94+
'bio' => "This is the bio for user {$userId}",
95+
'location' => 'Example City',
96+
],
97+
];
98+
99+
return [
100+
'uri' => $uri,
101+
'mimeType' => $this->mimeType,
102+
'text' => json_encode($userData, JSON_PRETTY_PRINT),
103+
];
104+
}
105+
}

src/Services/ResourceService/ResourceRepository.php

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,25 @@ public function registerResourceTemplate(ResourceTemplate|string $template): sel
7575
}
7676

7777
/**
78+
* Get all available resources including static resources and template-generated resources.
79+
*
7880
* @return array<int, array<string, mixed>>
7981
*/
8082
public function getResourceSchemas(): array
8183
{
82-
return array_values(array_map(fn (Resource $r) => $r->toArray(), $this->resources));
84+
$staticResources = array_values(array_map(fn (Resource $r) => $r->toArray(), $this->resources));
85+
86+
$templateResources = [];
87+
foreach ($this->templates as $template) {
88+
$listedResources = $template->list();
89+
if ($listedResources !== null) {
90+
foreach ($listedResources as $resource) {
91+
$templateResources[] = $resource;
92+
}
93+
}
94+
}
95+
96+
return array_merge($staticResources, $templateResources);
8397
}
8498

8599
/**
@@ -90,10 +104,32 @@ public function getTemplateSchemas(): array
90104
return array_values(array_map(fn (ResourceTemplate $t) => $t->toArray(), $this->templates));
91105
}
92106

93-
public function read(string $uri): ?array
107+
/**
108+
* Read resource content by URI.
109+
*
110+
* This method first attempts to find a static resource with an exact URI match.
111+
* If no static resource is found, it tries to match the URI against registered
112+
* resource templates and returns the dynamically generated content.
113+
*
114+
* @param string $uri The resource URI to read (e.g., "database://users/123")
115+
* @return array|null Resource content array with 'uri', 'mimeType', and 'text'/'blob', or null if not found
116+
*/
117+
public function readResource(string $uri): ?array
94118
{
119+
// First, try to find a static resource with exact URI match
95120
$resource = $this->resources[$uri] ?? null;
121+
if ($resource !== null) {
122+
return $resource->read();
123+
}
124+
125+
// If no static resource found, try to match against templates
126+
foreach ($this->templates as $template) {
127+
$params = $template->matchUri($uri);
128+
if ($params !== null) {
129+
return $template->read($uri, $params);
130+
}
131+
}
96132

97-
return $resource?->read();
133+
return null;
98134
}
99135
}

0 commit comments

Comments
 (0)