Skip to content

Commit ec2e5a6

Browse files
fix: DefinitionCache not in version control
1 parent e6d6cfc commit ec2e5a6

File tree

12 files changed

+229
-37
lines changed

12 files changed

+229
-37
lines changed

.gitignore

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,4 @@ workbench
6161
playground
6262

6363
# Log files
64-
*.log
65-
66-
# Cache files
67-
cache
64+
*.log

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![Tests](https://img.shields.io/github/actions/workflow/status/php-mcp/client/tests.yml?branch=main&style=flat-square)](https://github.com/php-mcp/client/actions/workflows/tests.yml)
66
[![License](https://img.shields.io/packagist/l/php-mcp/client.svg?style=flat-square)](LICENSE)
77

8-
** PHP MCP Client is a PHP library for interacting with servers that implement the Model Context Protocol (MCP).**
8+
**PHP MCP Client is a PHP library for interacting with servers that implement the Model Context Protocol (MCP).**
99

1010
It provides a developer-friendly interface to connect to individual MCP servers using different transports (`stdio`, `http+sse`), manage the connection lifecycle, discover server capabilities (Tools, Resources, Prompts), and execute requests like calling tools or reading resources.
1111

src/Cache/DefinitionCache.php

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMcp\Client\Cache;
6+
7+
use PhpMcp\Client\Exception\DefinitionException;
8+
use PhpMcp\Client\Model\Definitions\PromptDefinition;
9+
use PhpMcp\Client\Model\Definitions\ResourceDefinition;
10+
use PhpMcp\Client\Model\Definitions\ResourceTemplateDefinition;
11+
use PhpMcp\Client\Model\Definitions\ToolDefinition;
12+
use Psr\Log\LoggerInterface;
13+
use Psr\SimpleCache\CacheInterface;
14+
use Throwable;
15+
16+
/**
17+
* Handles caching of definitions (tools, resources, prompts) received from servers.
18+
* Uses a PSR-16 cache implementation provided via ClientConfig.
19+
*
20+
* @internal
21+
*/
22+
final class DefinitionCache
23+
{
24+
private const CACHE_KEY_PREFIX = 'mcp_client_defs_';
25+
26+
public function __construct(
27+
private readonly CacheInterface $cache,
28+
private readonly int $ttl, // Default TTL in seconds
29+
private readonly LoggerInterface $logger
30+
) {}
31+
32+
/**
33+
* @return array<ToolDefinition>|null Null if not found or error.
34+
*/
35+
public function getTools(string $serverName): ?array
36+
{
37+
return $this->get($serverName, 'tools', ToolDefinition::class);
38+
}
39+
40+
/**
41+
* @param array<ToolDefinition> $tools
42+
*/
43+
public function setTools(string $serverName, array $tools): void
44+
{
45+
$this->set($serverName, 'tools', $tools);
46+
}
47+
48+
/**
49+
* @return array<ResourceDefinition>|null
50+
*/
51+
public function getResources(string $serverName): ?array
52+
{
53+
return $this->get($serverName, 'resources', ResourceDefinition::class);
54+
}
55+
56+
/**
57+
* @param array<ResourceDefinition> $resources
58+
*/
59+
public function setResources(string $serverName, array $resources): void
60+
{
61+
$this->set($serverName, 'resources', $resources);
62+
}
63+
64+
/**
65+
* @return array<PromptDefinition>|null
66+
*/
67+
public function getPrompts(string $serverName): ?array
68+
{
69+
return $this->get($serverName, 'prompts', PromptDefinition::class);
70+
}
71+
72+
/**
73+
* @param array<PromptDefinition> $prompts
74+
*/
75+
public function setPrompts(string $serverName, array $prompts): void
76+
{
77+
$this->set($serverName, 'prompts', $prompts);
78+
}
79+
80+
/**
81+
* @return array<ResourceTemplateDefinition>|null
82+
*/
83+
public function getResourceTemplates(string $serverName): ?array
84+
{
85+
return $this->get($serverName, 'res_templates', ResourceTemplateDefinition::class);
86+
}
87+
88+
/**
89+
* @param array<ResourceTemplateDefinition> $templates
90+
*/
91+
public function setResourceTemplates(string $serverName, array $templates): void
92+
{
93+
$this->set($serverName, 'res_templates', $templates);
94+
}
95+
96+
/**
97+
* Generic get method.
98+
*
99+
* @template T of ToolDefinition|ResourceDefinition|PromptDefinition|ResourceTemplateDefinition
100+
*
101+
* @param class-string<T> $expectedClass FQCN of the definition class
102+
* @return array<T>|null
103+
*/
104+
private function get(string $serverName, string $type, string $expectedClass): ?array
105+
{
106+
$key = $this->generateCacheKey($serverName, $type);
107+
try {
108+
$cachedData = $this->cache->get($key);
109+
110+
if ($cachedData === null) {
111+
return null; // Cache miss
112+
}
113+
114+
if (! is_array($cachedData)) {
115+
$this->logger->warning("Invalid data type found in cache for {$key}. Expected array.", ['type' => gettype($cachedData)]);
116+
$this->cache->delete($key); // Clear invalid cache entry
117+
118+
return null;
119+
}
120+
121+
$definitions = [];
122+
foreach ($cachedData as $itemData) {
123+
if (! is_array($itemData)) {
124+
$this->logger->warning("Invalid item data type found in cached array for {$key}. Expected array.", ['type' => gettype($itemData)]);
125+
$this->cache->delete($key); // Clear invalid cache entry
126+
127+
return null;
128+
}
129+
// Re-hydrate from array using the static fromArray method
130+
if (! method_exists($expectedClass, 'fromArray')) {
131+
throw new DefinitionException("Definition class {$expectedClass} is missing the required fromArray method.");
132+
}
133+
134+
$definitions[] = call_user_func([$expectedClass, 'fromArray'], $itemData);
135+
}
136+
137+
return $definitions;
138+
139+
} catch (Throwable $e) {
140+
// Catch PSR-16 exceptions or hydration errors
141+
$this->logger->error("Error getting definitions from cache for key '{$key}': {$e->getMessage()}", ['exception' => $e]);
142+
143+
return null; // Return null on error
144+
}
145+
}
146+
147+
/**
148+
* Generic set method.
149+
*
150+
* @param array<ToolDefinition|ResourceDefinition|PromptDefinition|ResourceTemplateDefinition> $definitions
151+
*/
152+
private function set(string $serverName, string $type, array $definitions): void
153+
{
154+
$key = $this->generateCacheKey($serverName, $type);
155+
try {
156+
// Convert definition objects back to arrays for caching
157+
$dataToCache = array_map(function ($definition) {
158+
if (! method_exists($definition, 'toArray')) {
159+
throw new DefinitionException('Definition class '.get_class($definition).' is missing the required toArray method for caching.');
160+
}
161+
162+
return $definition->toArray(); // Assuming a toArray exists for caching
163+
}, $definitions);
164+
165+
$this->cache->set($key, $dataToCache, $this->ttl);
166+
} catch (Throwable $e) {
167+
// Catch PSR-16 exceptions or serialization errors
168+
$this->logger->error("Error setting definitions to cache for key '{$key}': {$e->getMessage()}", ['exception' => $e]);
169+
}
170+
}
171+
172+
private function generateCacheKey(string $serverName, string $type): string
173+
{
174+
// Sanitize server name for cache key safety
175+
$safeServerName = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $serverName);
176+
177+
return self::CACHE_KEY_PREFIX.$safeServerName.'_'.$type;
178+
}
179+
180+
/** Invalidate cache for a specific server */
181+
public function invalidateServerCache(string $serverName): void
182+
{
183+
$keys = [
184+
$this->generateCacheKey($serverName, 'tools'),
185+
$this->generateCacheKey($serverName, 'resources'),
186+
$this->generateCacheKey($serverName, 'prompts'),
187+
$this->generateCacheKey($serverName, 'res_templates'),
188+
];
189+
try {
190+
$this->cache->deleteMultiple($keys);
191+
$this->logger->info("Invalidated definition cache for server '{$serverName}'.");
192+
} catch (Throwable $e) {
193+
$this->logger->error("Error invalidating cache for server '{$serverName}': {$e->getMessage()}", ['exception' => $e]);
194+
}
195+
}
196+
}

src/JsonRpc/Error.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@
66

77
use Psy\Readline\Hoa\ProtocolException;
88

9-
final readonly class Error
9+
class Error
1010
{
1111
/**
1212
* @param mixed|null $data Optional additional data
1313
*/
1414
public function __construct(
15-
public int $code,
16-
public string $message,
17-
public mixed $data = null,
18-
) {
19-
}
15+
public readonly int $code,
16+
public readonly string $message,
17+
public readonly mixed $data = null,
18+
) {}
2019

2120
/**
2221
* @throws ProtocolException

src/JsonRpc/Params/InitializeParams.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
use PhpMcp\Client\Model\Capabilities;
88

9-
final readonly class InitializeParams
9+
class InitializeParams
1010
{
1111
public function __construct(
12-
public string $clientName,
13-
public string $clientVersion,
14-
public string $protocolVersion,
15-
public Capabilities $capabilities,
12+
public readonly string $clientName,
13+
public readonly string $clientVersion,
14+
public readonly string $protocolVersion,
15+
public readonly Capabilities $capabilities,
1616

1717
// Add optional processId, rootUri, trace etc. if client supports them
1818
) {}

src/JsonRpc/Params/SubscribeResourceParams.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
declare(strict_types=1);
44

5-
namespace PhpMcp\Client\JsonRpc\Parameter;
5+
namespace PhpMcp\Client\JsonRpc\Params;
66

7-
final readonly class SubscribeResourceParams
7+
class SubscribeResourceParams
88
{
9-
public function __construct(public string $uri) {}
9+
public function __construct(public readonly string $uri) {}
1010

1111
public function toArray(): array
1212
{

src/Model/Content/AudioContent.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
use PhpMcp\Client\Contracts\ContentInterface;
88
use PhpMcp\Client\Exception\ProtocolException;
99

10-
final readonly class AudioContent implements ContentInterface
10+
class AudioContent implements ContentInterface
1111
{
1212
public function __construct(
13-
public string $data,
14-
public string $mimeType
13+
public readonly string $data,
14+
public readonly string $mimeType
1515
) {}
1616

1717
public function getType(): string

src/Model/Content/EmbeddedResource.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66

77
use PhpMcp\Client\Exception\ProtocolException;
88

9-
final readonly class EmbeddedResource
9+
class EmbeddedResource
1010
{
1111
public function __construct(
12-
public string $uri,
13-
public string $mimeType,
14-
public ?string $text = null,
15-
public ?string $blob = null // Base64 encoded
12+
public readonly string $uri,
13+
public readonly string $mimeType,
14+
public readonly ?string $text = null,
15+
public readonly ?string $blob = null // Base64 encoded
1616
) {
1717
if ($this->text === null && $this->blob === null) {
1818
throw new \InvalidArgumentException("EmbeddedResource must have either 'text' or 'blob'.");

src/Model/Content/ImageContent.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
use PhpMcp\Client\Contracts\ContentInterface;
88
use PhpMcp\Client\Exception\ProtocolException;
99

10-
final readonly class ImageContent implements ContentInterface
10+
class ImageContent implements ContentInterface
1111
{
1212
public function __construct(
13-
public string $data,
14-
public string $mimeType
13+
public readonly string $data,
14+
public readonly string $mimeType
1515
) {}
1616

1717
public function getType(): string

src/Model/Content/PromptMessage.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
use PhpMcp\Client\Contracts\ContentInterface;
88
use PhpMcp\Client\Exception\ProtocolException;
99

10-
final readonly class PromptMessage
10+
class PromptMessage
1111
{
1212
public function __construct(
13-
public string $role,
14-
public ContentInterface $content
13+
public readonly string $role,
14+
public readonly ContentInterface $content
1515
) {
1616
if ($role !== 'user' && $role !== 'assistant') {
1717
throw new \InvalidArgumentException("Invalid role '{$role}', must be 'user' or 'assistant'.");

0 commit comments

Comments
 (0)