Skip to content

Conversation

butschster
Copy link
Contributor

@butschster butschster commented Sep 7, 2025

I would like to propose a refactoring of the Registry architecture to better align with SOLID principles and improve the overall design. This is currently a work-in-progress draft, and I welcome feedback and discussion on the approach.

Real-World Integration Issues

This particularly affects integration with validation libraries like Valinor:

// What developers want to achieve:
function (array $allLlmArguments) use ($valinorMapper, $schemaClass) {
    // Map ALL incoming LLM arguments to strongly-typed objects
    $validatedInput = $valinorMapper->map($schemaClass, $allLlmArguments);
    return $tool($validatedInput);
}

// What actually happens: $allLlmArguments is filtered and incomplete

The Interface Solution

ReferenceHandlerInterface enables:

  1. Custom Argument Processing: Implement handlers that preserve all arguments for libraries like Valinor
  2. Flexible Execution Strategies: Support different validation, logging, or preprocessing approaches
  3. Library Integration: Clean integration with existing PHP validation/mapping libraries
  4. Clear Extension Points: Well-defined contract for customization instead of fragile inheritance
class ValidationReferenceHandler implements ReferenceHandlerInterface 
{
    public function handle(ElementReference $reference, array $arguments): mixed 
    {
        // Full control over argument processing - no unexpected filtering
        // Can integrate with any validation library seamlessly
        return $this->executeWithCustomValidation($reference, $arguments);
    }
}

This interface transforms the rigid, filtering-based approach into a flexible, composable system that works with the PHP ecosystem rather than against it.

Usage Example

Before (Current Approach)

$registry = new Registry($referenceHandler);
$result = $registry->handleCallTool('tool_name', $arguments);

After (Proposed Approach)

$registry = new Registry(); // Only handles registration/access
$handler = new ReferenceHandler(); // Implements ReferenceHandlerInterface
$executor = new DefaultToolExecutor($registry, $handler);

$request = new CallToolRequest('tool_name', $arguments);
$result = $executor->call($request);

Custom Handler Example

class CustomReferenceHandler implements ReferenceHandlerInterface {
    public function handle(ElementReference $reference, array $arguments): mixed {
        // Custom pre-processing, logging, validation, etc.
        return $this->executeWithCustomLogic($reference, $arguments);
    }
}

Benefits

  • Single Responsibility: Registry focuses solely on managing registrations
  • Open/Closed Principle: Easy to extend with custom executors and handlers
  • Interface Segregation: Clients depend only on the interfaces they need
  • Dependency Inversion: Components depend on abstractions rather than concrete implementations
  • Enhanced Testability: Each component can be tested independently
  • Framework Consistency: Aligns with common patterns where providers and executors are separate

Dependency Injection and Container Integration

The interfaces are crucial for proper dependency injection and container bindings in modern PHP applications:

Registry Provider Binding

// In your DI container configuration
$container->bind(ReferenceProvider::class, Registry::class);
$container->bind(ReferenceRegistry::class, Registry::class);

// Or using singleton binding for shared registry
$container->singleton(Registry::class);
$container->bind(ReferenceProvider::class, fn($c) => $c->get(Registry::class));
$container->bind(ReferenceRegistry::class, fn($c) => $c->get(Registry::class));

Handler Interface Binding

// Bind different handlers for different environments
$container->when(DefaultToolExecutor::class)
    ->needs(ReferenceHandlerInterface::class)
    ->give(ReferenceHandler::class);

// Or for testing with mock handlers
$container->when('testing')
    ->bind(ReferenceHandlerInterface::class, MockReferenceHandler::class);

// Or custom validation handler for production
$container->when('production')
    ->bind(ReferenceHandlerInterface::class, ValidationReferenceHandler::class);

Tool Executor Binding

// Standard binding
$container->bind(ToolExecutorInterface::class, DefaultToolExecutor::class);

// Or with specific configurations per environment
$container->when(ApiController::class)
    ->needs(ToolExecutorInterface::class)
    ->give(CachedToolExecutor::class);

$container->when(BackgroundJobProcessor::class)
    ->needs(ToolExecutorInterface::class)
    ->give(AsyncToolExecutor::class);

Current Issue

The existing Registry class currently handles both registration and execution of MCP elements, which violates the Single Responsibility Principle. This creates several challenges:

  • Tight coupling between registration and execution logic
  • Difficulty in customizing execution behavior
  • Limited extensibility for different execution strategies
  • Inconsistent with common framework patterns where providers and executors are separate concerns

Proposed Solution

This refactoring introduces a cleaner separation of concerns by splitting the Registry responsibilities:

Interface Segregation

ReferenceProvider Interface - Focused on accessing registered elements:

interface ReferenceProviderInterface {
    public function getTool(string $name): ?ToolReference;
    public function getResource(string $uri, bool $includeTemplates = true): ResourceReference|ResourceTemplateReference|null;
    // ... other access methods
}

ReferenceRegistry Interface - Focused on managing registrations:

interface ReferenceRegistryInterface {
    public function registerTool(Tool $tool, callable|array|string $handler, bool $isManual = false): void;
    public function registerResource(Resource $resource, callable|array|string $handler, bool $isManual = false): void;
    // ... other registration methods
}

Handler Interface

ReferenceHandlerInterface - Enables custom execution strategies:

interface ReferenceHandlerInterface {
    public function handle(ElementReference $reference, array $arguments): mixed;
}

This allows users to implement their own handling logic while maintaining compatibility with the existing system.

Default Tool Executor

DefaultToolExecutor - Demonstrates the intended usage pattern:

class DefaultToolExecutor implements ToolExecutorInterface {
    public function __construct(
        private ReferenceProvider $referenceProvider,
        private ReferenceHandlerInterface $referenceHandler
    ) {}
    
    public function call(CallToolRequest $request): CallToolResult {
        // Clean execution logic using the interfaces
    }
}

Discussion Points

I would appreciate feedback on several aspects:

  • Does this level of separation feel appropriate for the use cases you envision?
  • Should we consider similar patterns for Resources and Prompts, or focus on Tools for now?
  • Are there any concerns about the interface design or naming conventions?
  • What would be a reasonable migration timeline for deprecating the old methods?

Why ReferenceHandlerInterface is Critical

The current ReferenceHandler class presents significant extensibility challenges that this refactoring addresses:

Current Limitations

The existing ReferenceHandler uses a rigid argument filtering mechanism that breaks custom callable patterns:

// Current ReferenceHandler filters arguments based on parameter names
private function prepareArguments(\ReflectionFunctionAbstract $reflection, array $arguments): array
{
    foreach ($reflection->getParameters() as $parameter) {
        $paramName = $parameter->getName();
        // Only arguments with matching parameter names survive
        if (isset($arguments[$paramName])) {
            $finalArgs[$paramPosition] = $arguments[$paramName];
        }
    }
    return array_values($finalArgs);
}

The Problem with Custom Wrappers

This filtering breaks patterns where developers want to implement custom argument processing:

// This pattern fails with current ReferenceHandler:
$registry->registerTool($tool, function (array $arguments) use ($schemaMapper, $schemaClass) {
    // $arguments arrives empty because ReferenceHandler filters out
    // all LLM arguments that don't match the parameter name 'arguments'
    $validatedObject = $schemaMapper->toObject(json_encode($arguments), $schemaClass);
    return $tool($validatedObject);
});

Context Note

The ToolExecutorInterface was already present in the codebase but wasn't being utilized as the primary execution mechanism. This refactoring makes it the central component for tool execution as originally intended, rather than just an extension point that wasn't actively used.

Next Steps

If this approach looks reasonable, I would like to:

  1. Gather feedback on the design
  2. Add comprehensive tests for the new components
  3. Create similar patterns for Resource and Prompt execution
  4. Update documentation and examples

Thank you for taking the time to review this proposal. I look forward to your thoughts and suggestions.

@chr-hertel
Copy link
Member

Hey @butschster, first of all thanks for kicking off that discussion - I think there are a couple of instance in our current code base where we're mixing too many concerns in one class, as well state vs service classes. so, yes, let's work on this.

there is #39 going on at the same time, and i think it would be easier to pull off after this PR here - @xentixar what do you think?

(need to give your proposed slicing a deeper look tho later)

@butschster
Copy link
Contributor Author

Hello, @chr-hertel ! 👋

As you noticed, I’ve taken the time to study the project thoroughly. In this pull request, I tried to demonstrate how I see the structure evolving.

The example I used comes from my production MCP servers, which are based on @CodeWithKyrian’s MCP package. This gave me the opportunity to validate the approach in a real-world environment and think through how the structure could be reorganized more effectively.

If you like the direction, I’m ready to finish the task.

@xentixar
Copy link
Contributor

xentixar commented Sep 7, 2025

@chr-hertel Thanks for bringing this up! I agree that it would make sense to complete the Registry architecture refactoring in PR 46 first, as it will likely simplify the implementation in PR 39 and reduce potential conflicts.

The separation of concerns approach outlined in this PR aligns well with what I'm working on, and having the cleaner interfaces in place will definitely make the integration smoother.

I'm happy to wait for this refactoring to be merged before proceeding with my work. This will also give me a chance to review the new architecture and ensure my implementation follows the improved patterns.

Thanks for coordinating this - it's much better to get the foundation right first!

@butschster
Copy link
Contributor Author

@chr-hertel could you please run actions to check tests and cs?

@butschster
Copy link
Contributor Author

I think I’m done with this iteration, but a couple of notes:

  • The ResourceTemplateReference::read method is currently marked as deprecated. I believe this functionality should really live in a dedicated Reader for template references rather than inside the reference itself.

  • In ListPromptsHandler::handle, ListResourcesHandler::handle, and ListToolsHandler::handle we currently have calls like:

    $tools = $this->registry->getTools($this->pageSize, $message->cursor);

However, getTools (and the similar methods) don’t actually accept $pageSize or $cursor as arguments right now. This probably needs alignment.

Should I get rid of arguments or add some todo?

If a README or usage notes for the new features would help, let me know and I can add one. 🚀

@butschster
Copy link
Contributor Author

butschster commented Sep 8, 2025

@chr-hertel @xentixar Ready to review

I added unit tests for the code affected by my changes and fixed the PHPStan issues, except for those that need to be discussed and addressed collaboratively and that are unrelated to my changes.

After writing the tests, I found a small issue and created an issue for it #50

Copy link
Member

@chr-hertel chr-hertel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great step in the right direction - left a few comments, but will revisit after the diff is cleaned up, and pipeline is green.

Thanks already! 👍

@butschster
Copy link
Contributor Author

@chr-hertel done

@butschster
Copy link
Contributor Author

butschster commented Sep 9, 2025

@chr-hertel I want to merge changes from #53 to my PR after merging. Ready to wait

@chr-hertel
Copy link
Member

@chr-hertel I want to merge changes from #53 to my PR after merging. Ready to wait

Fine by me 👍


One thing i wonder, what do you think about repository pattern here - per type of capability?
And not the doctrine interpretation of it, which is only intended for finding stuff, but the pattern like Martin Fowler describes it.

Yes, we're not dealing with a database engine here, true, but with #39 we'd need to think about state here as well.

And going forward with a per-capability-design, would potentially ease to bring in per-capability-infrastructure in list/get request handlers as well.


don't get me wrong, this PR is def an improvement - just a thought :)

- Extract ReferenceProvider and ReferenceRegistryInterface interfaces
- Create DefaultToolExecutor with ReferenceHandlerInterface
- Remove execution responsibility from Registry class
- Enable custom handler and executor implementations
* Create DefaultResourceReader, DefaultPromptGetter
* Refactor JsonRpc Handler and RequestHandlers to use dedicated executors
* Update ServerBuilder to support custom executors via dependency injection
@butschster
Copy link
Contributor Author

@chr-hertel @CodeWithKyrian done

@butschster
Copy link
Contributor Author

butschster commented Sep 9, 2025

@chr-hertel I want to merge changes from #53 to my PR after merging. Ready to wait

Fine by me 👍

One thing i wonder, what do you think about repository pattern here - per type of capability? And not the doctrine interpretation of it, which is only intended for finding stuff, but the pattern like Martin Fowler describes it.

Yes, we're not dealing with a database engine here, true, but with #39 we'd need to think about state here as well.

And going forward with a per-capability-design, would potentially ease to bring in per-capability-infrastructure in list/get request handlers as well.

don't get me wrong, this PR is def an improvement - just a thought :)

Hi @chr-hertel, thanks for raising this point!

From my experience building a more complex tool CTX that already embeds an MCP server inside, I tend to agree with the idea of splitting things up per capability.

If I understood your question correctly, here’s my perspective: an MCP server usually exposes several types of elements — tools, resources, prompts, and prompt templates. Each of these can come from different sources:

  • Prompts can be stored in a database or loaded from local files.
  • Resources might be part of the codebase or references to remote services.
  • Tools can be either internal (PHP classes) or external scripts.

Because of this variety, a single “global” repository abstraction quickly becomes too rigid. Sooner or later we’ll need the flexibility to aggregate from multiple sources per type. That’s where having dedicated repositories per capability — or even composite repositories — really pays off. They allow us to cleanly plug in new sources without overloading one central abstraction.

In practice, this makes the design easier to extend and reason about, especially when different types of resources evolve at different speeds and have very different lifecycles.

So personally, I see “more granularity” here as a net positive, and a repository-per-capability approach would give us the room to support heterogeneous sources without bending the model too much.

There are repositories for each resource type - ResourceRegistry, ToolRegistry, PromptRegistry?

@butschster
Copy link
Contributor Author

@chr-hertel @CodeWithKyrian I fixed code style. phpstan will be fixed here #55

@bigdevlarry
Copy link
Contributor

bigdevlarry commented Sep 10, 2025

Hi @butschster, the PHPStan issues are related to your pull request. Please take a look. It would be easier if you resolved them now, so I can pull into my branch if your PR gets merged first. If my work gets merged first, you can update your branch afterward, as I've fixed the PHPStan issues in #55.

@butschster
Copy link
Contributor Author

Hi @butschster, the PHPStan issues are related to your pull request. Please take a look. It would be easier if you resolved them now, so I can pull into my branch if your PR gets merged first. If my work gets merged first, you can update your branch afterward, as I've fixed the PHPStan issues in #55.

Hi @bigdevlarry, thanks for pointing this out. Just to clarify: the PHPStan issues you’re seeing were already present before my changes. For example, pagination logic wasn’t working initially and still needs additional work, so I didn’t touch it here since my focus was on other parts of the code.

Another reported issue is related to the examples — that code was also non-functional from the start. I left it as is to avoid bloating this pull request with unrelated fixes.

So I’d suggest handling those issues separately, outside of this PR.

@butschster
Copy link
Contributor Author

butschster commented Sep 10, 2025

Hi @chr-hertel!
I’ve added the pagination and example issues to phpstan ignore. Now PHPStan should be green.

@butschster
Copy link
Contributor Author

butschster commented Sep 11, 2025

I’ve completed all the changes for this pull request, and all tests and qa are passing.
Just wanted to check in — is there anything else needed from my side before we can move forward?

Thanks a lot for your time and feedback!

Copy link
Member

@chr-hertel chr-hertel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my point of view this is good to get merged 👍

I think we could go on with a per-capability approach on top of that, and I'm not sure about the design of every interface, but it's a great next step.

@CodeWithKyrian do you agree?

@CodeWithKyrian
Copy link
Contributor

Totally agree. Looks solid to me. Thanks for the great work on this, @butschster 👍🏽

@chr-hertel chr-hertel merged commit 99bf56e into modelcontextprotocol:main Sep 14, 2025
9 checks passed
@chr-hertel
Copy link
Member

Thanks @butschster 🙌

@butschster butschster deleted the feature/tool-execution branch September 14, 2025 18:45
CodeWithKyrian added a commit to CodeWithKyrian/php-sdk that referenced this pull request Sep 14, 2025
CodeWithKyrian added a commit to CodeWithKyrian/php-sdk that referenced this pull request Sep 20, 2025
chr-hertel pushed a commit that referenced this pull request Sep 21, 2025
…nsport (#49)

* feat(server): Rework transport architecture and add StreamableHttpTransport

- `Server::connect()` no longer contains a processing loop.
- `TransportInterface` is updated with `setMessageHandler()` and `listen()`.
- `StdioTransport` is updated to implement the new interface.
- A new, minimal `StreamableHttpTransport` is added for stateless HTTP.

* refactor(server): use  event-driven message handling

* feat(server): Introduce a formal session management system

* fix: ensure session elements are preserved when building server (regression from #46)

* refactor: consolidate HTTP example to use shared dependencies

* feat(server): Enhance message handling with session support

- Updated `TransportInterface` to use `onMessage` for handling incoming messages with session IDs.
- Refactored `Server`, `Handler`, and transport classes to accommodate session management using `Uuid`.
- Introduced methods for creating sessions with auto-generated and specific UUIDs in `SessionFactory` and `SessionFactoryInterface`.

* feat(server): Integrate session management in message handling

- Added session support to the `Server` and `Handler` classes, allowing for session data to be managed during message processing.
- Updated `TransportInterface` to include session context in the `send` method.
- Refactored various request handlers to utilize session information, ensuring proper session handling for incoming requests.
- Introduced a file-based session store for persistent session data management

* feat: Update session builder method to use set* instead of with*

* feat: Fix test compatibility with new session management architecture

* chore: apply code style fixes

* fix: remove deprecated SSE transport and resolve PHPStan issues
@chr-hertel chr-hertel changed the title Registry Architecture Refactoring - Enhanced Separation of Concerns [Server] Registry Architecture Refactoring - Enhanced Separation of Concerns Sep 21, 2025
@chr-hertel chr-hertel added the Server Issues & PRs related to the Server component label Sep 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Server Issues & PRs related to the Server component Status: Needs Decision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants