Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to `mcp/sdk` will be documented in this file.

### Server

* Add `Builder::setFiberFactory()` to let integrations propagate execution context into handler fibers in both protocol eras.
* [BC Break] Require `phpdocumentor/reflection-docblock` `^6.0`, and with it `phpdocumentor/type-resolver` `^2.0` and `phpstan/phpdoc-parser` `^2.0`. Older versions cannot parse types like `non-empty-array<T>`, and the SDK silently ignores such a `@param`.
* Require `psr/container` `^1.1`, as `Container` is not compatible with `1.0`.
* [BC Break] `DocBlockParser::parseDocBlock()` no longer accepts `null`; pass `ReflectionMethod::getDocComment()` as is
Expand Down
40 changes: 40 additions & 0 deletions docs/run/server-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,45 @@ $server = Server::builder()
->setEventDispatcher($eventDispatcher);
```

### Handler Fiber Context

Handlers run in fibers, which do not automatically inherit context from the caller.
Use `setFiberFactory()` to propagate context such as the active OpenTelemetry span:

```php
use Mcp\Server;
use Mcp\Server\FiberFactoryInterface;
use OpenTelemetry\Context\Context;

$server = Server::builder()
->setFiberFactory(new class implements FiberFactoryInterface {
public function create(\Closure $callback): \Fiber
{
$context = Context::getCurrent();

return new \Fiber(static function () use ($context, $callback) {
$scope = $context->activate();
try {
return $callback();
} finally {
$scope->detach();
}
});
}
})
->build();
```

The factory runs in the parent context and must return an **unstarted** fiber that invokes the
callback once, preserving its return value, exceptions and suspensions. Attach context inside
the fiber and release it in `finally`, including when a suspended fiber is discarded.
The SDK continues to start and resume the fiber.

This example uses OpenTelemetry's default fiber-bound context storage, which keeps contexts
isolated across suspensions and resumptions. It requires the application's `open-telemetry/context`
package; the SDK does not depend on OpenTelemetry. The factory applies to both protocol eras,
including `buildStateless()`. Without a factory, the SDK creates ordinary PHP fibers as before.

## Complete Example

Here's a comprehensive example showing all major configuration options:
Expand Down Expand Up @@ -352,6 +391,7 @@ $server = Server::builder()
| `setLogger()` | logger | Set PSR-3 logger |
| `setContainer()` | container | Set PSR-11 container |
| `setEventDispatcher()` | eventDispatcher | Set PSR-14 event dispatcher, see [Events](../advanced/events.md) |
| `setFiberFactory()` | fiberFactory | Propagate execution context into handler fibers |
| `setSchemaGenerator()` | schemaGenerator | Replace the generator that builds input schemas from PHP types, see [Schema generation](../servers/schemas.md) |
| `setSchemaValidator()` | schemaValidator | Replace the validator for tool arguments and structured output, see [Schema generation](../servers/schemas.md) |
| `setReferenceHandler()` | referenceHandler | Replace the class that calls the handlers of tools, resources and prompts |
Expand Down
10 changes: 10 additions & 0 deletions src/Server/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ final class Builder
private CacheInterface|CacheItemPoolInterface|null $discoveryCache = null;

private ?EventDispatcherInterface $eventDispatcher = null;
private ?FiberFactoryInterface $fiberFactory = null;

private ?ContainerInterface $container = null;

Expand Down Expand Up @@ -600,6 +601,13 @@ public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): s
return $this;
}

public function setFiberFactory(FiberFactoryInterface $fiberFactory): self
{
$this->fiberFactory = $fiberFactory;

return $this;
}

/**
* Provides a PSR-11 DI container, primarily for resolving user-defined handler classes.
* Defaults to a basic internal container.
Expand Down Expand Up @@ -1003,6 +1011,7 @@ public function build(): Server
: null,
requestStateCodec: $this->requestStateCodec(),
sessionLock: $this->sessionLock,
fiberFactory: $this->fiberFactory,
);

$modernVersions = $this->modernVersions ?? ProtocolVersion::modernVersions();
Expand Down Expand Up @@ -1047,6 +1056,7 @@ public function buildStateless(array $supportedVersions = [ProtocolVersion::V202
extensionMethods: $this->extensionMethods,
eventDispatcher: $parts['eventDispatcher'],
requestStateBinder: $this->requestStateBinder,
fiberFactory: $this->fiberFactory,
);
}

Expand Down
31 changes: 31 additions & 0 deletions src/Server/FiberFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Server;

/**
* Creates handler fibers with the caller's execution context.
*
* @author bbdu3 <ergouyang854@gamil.com>
*/
interface FiberFactoryInterface
{
/**
* Returns an unstarted fiber invoking the callback once, preserving its return value, exceptions and suspensions.
*
* @template TReturn
*
* @param \Closure(): TReturn $callback
*
* @return \Fiber<mixed, mixed, TReturn, mixed>
*/
public function create(\Closure $callback): \Fiber;
}
8 changes: 5 additions & 3 deletions src/Server/Protocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public function __construct(
private readonly ?InputRequiredShim $inputRequiredShim = null,
private readonly ?RequestStateCodec $requestStateCodec = null,
private readonly ?SessionLockInterface $sessionLock = null,
private readonly ?FiberFactoryInterface $fiberFactory = null,
) {
$this->awaitedRequestIds = new \WeakMap();
$this->fiberOutgoingMessages = new \WeakMap();
Expand Down Expand Up @@ -411,8 +412,7 @@ private function handleRequest(TransportInterface $transport, Request $request,
// One fiber for the whole exchange: with the shim, the handler
// re-enters inside it each round rather than needing a new one.
// Handled in the fiber, as the transport resumes it outside the catch below.
/** @var McpFiber $fiber */
$fiber = new \Fiber(static function () use ($handler, $request, $session, $shim, $codec, $dispatchResult, $errorForThrowable, $saveResumedSession, &$suspended): Response|Error {
$callback = static function () use ($handler, $request, $session, $shim, $codec, $dispatchResult, $errorForThrowable, $saveResumedSession, &$suspended): Response|Error {
try {
$result = $handler->handle($request, $session);
$result = $shim?->fulfill($result, $handler, $request, $session, $codec) ?? $result;
Expand All @@ -427,7 +427,9 @@ private function handleRequest(TransportInterface $transport, Request $request,
}

return $result;
});
};
/** @var McpFiber $fiber */
$fiber = $this->fiberFactory?->create($callback) ?? new \Fiber($callback);

$result = $fiber->start();

Expand Down
5 changes: 4 additions & 1 deletion src/Server/Stateless/StatelessProtocol.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Mcp\Schema\Result\InputRequiredResult;
use Mcp\Server\Authorization\AccessToken;
use Mcp\Server\Configuration;
use Mcp\Server\FiberFactoryInterface;
use Mcp\Server\Handler\Request\RequestHandlerInterface;
use Mcp\Server\Protocol;
use Mcp\Server\Session\InMemorySessionStore;
Expand Down Expand Up @@ -114,6 +115,7 @@ public function __construct(
private readonly array $extensionMethods = [],
private readonly ?EventDispatcherInterface $eventDispatcher = null,
private readonly ?RequestStateBinderInterface $requestStateBinder = null,
private readonly ?FiberFactoryInterface $fiberFactory = null,
) {
$this->codec = $codec ?? new Rev2026Codec($configuration->serverInfo, $cachePolicy);

Expand Down Expand Up @@ -693,7 +695,8 @@ private function unknownMethod(string $method, string|int|null $id): Error
*/
private function run(RequestHandlerInterface $handler, Request $request, Session $session, RequestMeta $meta): \Generator
{
$fiber = new \Fiber(static fn (): mixed => $handler->handle($request, $session));
$callback = static fn (): mixed => $handler->handle($request, $session);
$fiber = $this->fiberFactory?->create($callback) ?? new \Fiber($callback);
$input = $session->get(InputContext::class);
// The codec bound to this request, which dispatch() put on the session.
$codec = $session->get(RequestStateCodec::class);
Expand Down
56 changes: 56 additions & 0 deletions tests/Unit/Fixtures/ContextFiberFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Tests\Unit\Fixtures;

use Mcp\Exception\LogicException;
use Mcp\Server\FiberFactoryInterface;

/**
* @author bbdu3 <ergouyang854@gamil.com>
*/
final class ContextFiberFactory implements FiberFactoryInterface
{
public string $parent = 'parent';
public int $created = 0;
public int $released = 0;

/** @var \WeakMap<object, string> */
private \WeakMap $contexts;

public function __construct()
{
$this->contexts = new \WeakMap();
}

public function create(\Closure $callback): \Fiber
{
$context = $this->current();
++$this->created;

return new \Fiber(function () use ($context, $callback) {
$this->contexts[\Fiber::getCurrent() ?? throw new LogicException('No active fiber.')] = $context;
try {
return $callback();
} finally {
unset($this->contexts[\Fiber::getCurrent()]);
++$this->released;
}
});
}

public function current(): string
{
$fiber = \Fiber::getCurrent();

return null === $fiber ? $this->parent : $this->contexts[$fiber];
}
}
Loading
Loading