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
11 changes: 3 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,10 @@ The server uses a decoupled architecture:

* **`ServerBuilder`:** Fluent interface (`Server::make()->...`) for configuration. Collects server identity, dependencies (Logger, Cache, Container, Loop), capabilities, and **manual** element registrations. Calls `build()` to create the `Server` instance.
* **`Configuration`:** A value object holding the resolved configuration and dependencies.
* **`Server`:** The central object holding the configured state and core logic components (`Registry`, `Processor`, `ClientStateManager`, `Configuration`). It's transport-agnostic. Provides methods to `discover()` elements and `listen()` via a specific transport.
* **`Server`:** The central object holding the configured state and core logic components (`Registry`, `Protocol`, `Configuration`). It's transport-agnostic. Provides methods to `discover()` elements and `listen()` via a specific transport.
* **`Protocol`:** Internal bridge listening to transport events and processes JSON-RPC messages from the transport.
* **`Registry`:** Stores MCP element definitions. **Distinguishes between manually registered and discovered elements.** Handles optional caching of *discovered* elements only. Loads cached discovered elements upon instantiation if available.
* **`Processor`:** Processes parsed JSON-RPC requests/notifications, executes handlers (via DI Container), formats results, handles errors.
* **`SchemaGenerator`:** Generates JSON Schema for method parameters from PHP type hints and DocBlocks.
* **`SchemaValidator`:** Validates incoming data against the generated JSON Schema.
* **`ClientStateManager`:** Manages client runtime state (initialization, subscriptions, activity) using the configured cache.
* **`ServerTransportInterface`:** Event-driven interface for server-side transports (`StdioServerTransport`, `HttpServerTransport`). Handles communication, emits events.
* **`Protocol`:** Internal bridge listening to transport events, interacting with `Processor` and `ClientStateManager`.
* **`discover()` Method:** An **explicit** method on the `Server` instance to trigger attribute discovery. Takes path configurations as arguments. By default, it clears previously discovered/cached elements before scanning and saves the new results to cache (if enabled).
* **`listen()` Method:** Starts the server using a specific transport. Binds the `Protocol`, starts the transport listener, and **runs the event loop (blocking)**. Performs a pre-check and warns if no elements are registered and discovery hasn't run.

## Defining MCP Elements

Expand Down Expand Up @@ -229,6 +223,7 @@ You can get a server builder instance by either calling `new ServerBuilder` or m
* **`withContainer(ContainerInterface $container)`**: Optional. PSR-11 container for resolving *your handler classes*. Defaults to `BasicContainer`.
* **`withLoop(LoopInterface $loop)`**: Optional. ReactPHP event loop. Defaults to `Loop::get()`.
* **`withCapabilities(Capabilities $capabilities)`**: Optional. Configure advertised capabilities (e.g., resource subscriptions). Use `Capabilities::forServer(...)`.
* **`withPaginationLimit(int $paginationLimit)`: Optional. Configures the server's pagination limit for list requests.
* `withTool(...)`, `withResource(...)`, etc.: Optional manual registration.

## Running the Server (Transports)
Expand Down
4 changes: 2 additions & 2 deletions examples/01-discovery-stdio-calculator/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function log($level, \Stringable|string $message, array $context = []): v
}

try {
$logger = new StderrLogger;
$logger = new StderrLogger();
$logger->info('Starting MCP Stdio Calculator Server...');

$server = Server::make()
Expand All @@ -66,7 +66,7 @@ public function log($level, \Stringable|string $message, array $context = []): v

$server->discover(__DIR__, ['.']);

$transport = new StdioServerTransport;
$transport = new StdioServerTransport();

$server->listen($transport);

Expand Down
4 changes: 2 additions & 2 deletions examples/02-discovery-http-userprofile/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ public function log($level, \Stringable|string $message, array $context = []): v
}

try {
$logger = new StderrLogger;
$logger = new StderrLogger();
$logger->info('Starting MCP HTTP User Profile Server...');

// --- Setup DI Container for DI in McpElements class ---
$container = new BasicContainer;
$container = new BasicContainer();
$container->set(LoggerInterface::class, $logger);

$server = Server::make()
Expand Down
6 changes: 3 additions & 3 deletions examples/03-manual-registration-stdio/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ public function log($level, \Stringable|string $message, array $context = []): v
}

try {
$logger = new StderrLogger;
$logger = new StderrLogger();
$logger->info('Starting MCP Manual Registration (Stdio) Server...');

$container = new BasicContainer;
$container = new BasicContainer();
$container->set(LoggerInterface::class, $logger);

$server = Server::make()
Expand All @@ -67,7 +67,7 @@ public function log($level, \Stringable|string $message, array $context = []): v
->withResourceTemplate([SimpleHandlers::class, 'getItemDetails'], 'item://{itemId}/details', 'get_item_details', mimeType: 'application/json')
->build();

$transport = new StdioServerTransport;
$transport = new StdioServerTransport();
$server->listen($transport);

$logger->info('Server listener stopped gracefully.');
Expand Down
4 changes: 2 additions & 2 deletions examples/04-combined-registration-http/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ public function log($level, \Stringable|string $message, array $context = []): v
}

try {
$logger = new StderrLogger;
$logger = new StderrLogger();
$logger->info('Starting MCP Combined Registration (HTTP) Server...');

$container = new BasicContainer;
$container = new BasicContainer();
$container->set(LoggerInterface::class, $logger); // ManualHandlers needs LoggerInterface

$server = Server::make()
Expand Down
4 changes: 3 additions & 1 deletion examples/05-stdio-env-variables/EnvToolHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

class EnvToolHandler
{
public function __construct() {}
public function __construct()
{
}

/**
* Performs an action that can be modified by an environment variable.
Expand Down
4 changes: 2 additions & 2 deletions examples/05-stdio-env-variables/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public function log($level, \Stringable|string $message, array $context = []): v
}

try {
$logger = new StderrLogger;
$logger = new StderrLogger();
$logger->info('Starting MCP Stdio Environment Variable Example Server...');

$server = Server::make()
Expand All @@ -61,7 +61,7 @@ public function log($level, \Stringable|string $message, array $context = []): v

$server->discover(__DIR__, ['.']);

$transport = new StdioServerTransport;
$transport = new StdioServerTransport();
$server->listen($transport);

$logger->info('Server listener stopped gracefully.');
Expand Down
6 changes: 3 additions & 3 deletions examples/06-custom-dependencies-stdio/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ public function log($level, \Stringable|string $message, array $context = []): v
}

try {
$logger = new StderrLogger;
$logger = new StderrLogger();
$logger->info('Starting MCP Custom Dependencies (Stdio) Server...');

$container = new BasicContainer;
$container = new BasicContainer();
$container->set(LoggerInterface::class, $logger);

$taskRepo = new Services\InMemoryTaskRepository($logger);
Expand All @@ -89,7 +89,7 @@ public function log($level, \Stringable|string $message, array $context = []): v

$server->discover(__DIR__, ['.']);

$transport = new StdioServerTransport;
$transport = new StdioServerTransport();
$server->listen($transport);

$logger->info('Server listener stopped gracefully.');
Expand Down
4 changes: 2 additions & 2 deletions examples/07-complex-tool-schema-http/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ public function log($level, \Stringable|string $message, array $context = []): v
}

try {
$logger = new StderrLogger;
$logger = new StderrLogger();
$logger->info('Starting MCP Complex Schema HTTP Server...');

$container = new BasicContainer;
$container = new BasicContainer();
$container->set(LoggerInterface::class, $logger);

$server = Server::make()
Expand Down
84 changes: 58 additions & 26 deletions src/Attributes/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Schema
{
/** @var Property[] */
protected array $properties = [];

/**
* @param string|null $format String format (email, date-time, uri, etc.)
* @param int|null $minLength Minimum string length
Expand Down Expand Up @@ -63,26 +63,52 @@ public function __construct(
public function toArray(): array
{
$schema = [];

// String constraints
if ($this->format !== null) $schema['format'] = $this->format;
if ($this->minLength !== null) $schema['minLength'] = $this->minLength;
if ($this->maxLength !== null) $schema['maxLength'] = $this->maxLength;
if ($this->pattern !== null) $schema['pattern'] = $this->pattern;

if ($this->format !== null) {
$schema['format'] = $this->format;
}
if ($this->minLength !== null) {
$schema['minLength'] = $this->minLength;
}
if ($this->maxLength !== null) {
$schema['maxLength'] = $this->maxLength;
}
if ($this->pattern !== null) {
$schema['pattern'] = $this->pattern;
}

// Numeric constraints
if ($this->minimum !== null) $schema['minimum'] = $this->minimum;
if ($this->maximum !== null) $schema['maximum'] = $this->maximum;
if ($this->exclusiveMinimum !== null) $schema['exclusiveMinimum'] = $this->exclusiveMinimum;
if ($this->exclusiveMaximum !== null) $schema['exclusiveMaximum'] = $this->exclusiveMaximum;
if ($this->multipleOf !== null) $schema['multipleOf'] = $this->multipleOf;

if ($this->minimum !== null) {
$schema['minimum'] = $this->minimum;
}
if ($this->maximum !== null) {
$schema['maximum'] = $this->maximum;
}
if ($this->exclusiveMinimum !== null) {
$schema['exclusiveMinimum'] = $this->exclusiveMinimum;
}
if ($this->exclusiveMaximum !== null) {
$schema['exclusiveMaximum'] = $this->exclusiveMaximum;
}
if ($this->multipleOf !== null) {
$schema['multipleOf'] = $this->multipleOf;
}

// Array constraints
if ($this->items !== null) $schema['items'] = $this->items->toArray();
if ($this->minItems !== null) $schema['minItems'] = $this->minItems;
if ($this->maxItems !== null) $schema['maxItems'] = $this->maxItems;
if ($this->uniqueItems !== null) $schema['uniqueItems'] = $this->uniqueItems;

if ($this->items !== null) {
$schema['items'] = $this->items->toArray();
}
if ($this->minItems !== null) {
$schema['minItems'] = $this->minItems;
}
if ($this->maxItems !== null) {
$schema['maxItems'] = $this->maxItems;
}
if ($this->uniqueItems !== null) {
$schema['uniqueItems'] = $this->uniqueItems;
}

// Object constraints
if (!empty($this->properties)) {
$props = [];
Expand All @@ -91,21 +117,27 @@ public function toArray(): array
}
$schema['properties'] = $props;
}

if ($this->required !== null) $schema['required'] = $this->required;


if ($this->required !== null) {
$schema['required'] = $this->required;
}

if ($this->additionalProperties !== null) {
if ($this->additionalProperties instanceof self) {
$schema['additionalProperties'] = $this->additionalProperties->toArray();
} else {
$schema['additionalProperties'] = $this->additionalProperties;
}
}

// General constraints
if ($this->enum !== null) $schema['enum'] = $this->enum;
if ($this->default !== null) $schema['default'] = $this->default;

if ($this->enum !== null) {
$schema['enum'] = $this->enum;
}
if ($this->default !== null) {
$schema['default'] = $this->default;
}

return $schema;
}
}
}
2 changes: 1 addition & 1 deletion src/Attributes/Schema/ArrayItems.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ enum: $enum,
default: $default,
);
}
}
}
2 changes: 1 addition & 1 deletion src/Attributes/Schema/Format.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ class Format
public const RELATIVE_JSON_POINTER = 'relative-json-pointer';
public const REGEX = 'regex';
public const UUID = 'uuid';
}
}
2 changes: 1 addition & 1 deletion src/Attributes/Schema/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ enum: $enum,
default: $default,
);
}
}
}
5 changes: 3 additions & 2 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Configuration
* @param CacheInterface|null $cache Optional PSR-16 Cache instance for registry/state.
* @param ContainerInterface $container PSR-11 DI Container for resolving handlers/dependencies.
* @param int $definitionCacheTtl TTL in seconds for cached definitions (if cache is provided).
* @param int $paginationLimit Maximum number of items to return for list methods.
*/
public function __construct(
public readonly string $serverName,
Expand All @@ -36,6 +37,6 @@ public function __construct(
public readonly ?CacheInterface $cache,
public readonly ContainerInterface $container,
public readonly int $definitionCacheTtl = 3600,
) {
}
public readonly int $paginationLimit = 50,
) {}
}
2 changes: 1 addition & 1 deletion src/Defaults/ArrayCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private function calculateExpiry(DateInterval|int|null $ttl): ?int
return time() + $ttl;
}
if ($ttl instanceof DateInterval) {
return (new DateTime)->add($ttl)->getTimestamp();
return (new DateTime())->add($ttl)->getTimestamp();
}

// Invalid TTL type, treat as no expiry
Expand Down
Loading