Skip to content
This repository was archived by the owner on Nov 12, 2025. It is now read-only.

Commit 0b74d06

Browse files
committed
feat(mcp): support STDIO transport
1 parent 68447f5 commit 0b74d06

11 files changed

Lines changed: 1273 additions & 588 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.4.0] - 2025-07-28
9+
10+
### Added
11+
12+
- Added support for STDIO transport through a Symfony Command : `mcp:stdio`
13+
- Added a new CommandTestCase for handling tests of the Symfony command
14+
15+
### Updated
16+
17+
- Updated the test to be more reusable, by creating a trait that contains DataProviders and assertion logic
18+
819
## [1.3.0] - 2025-07-27
920

1021
### Updated

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ The current MCP protocol supported version is `2025-06-18`, which is the latest
1919
> This bundle will evolve along with the specification, so please ensure you are using the latest version of the bundle.
2020
> The CHANGELOG can be found [here](CHANGELOG.md).
2121
22+
The MCP Server Bundle currently supports HTTP & STDIO as transports.
23+
2224
## Table of Contents
2325

2426
- [Getting Started](#getting-started)
2527
- [Installation](#installation)
2628
- [Configuration](#configuration)
29+
- [Transports](#transports)
2730
- [Tools](#tools)
2831
- [Creating Tools](#creating-tools)
2932
- [Tool Results](#tool-results)
@@ -93,6 +96,30 @@ mcp_server:
9396
version: '1.0.0' # The version of your MCP server, used in the initialization response
9497
```
9598

99+
### Transports
100+
101+
This bundle adds support for HTTP and STDIO transports.
102+
103+
#### HTTP
104+
105+
HTTP support is featured using a controller, as stated in the [configuration](#configuration).
106+
107+
#### STDIO
108+
109+
STDIO transport is featured through a Symfony command:
110+
```bash
111+
$ bin/console mcp:stdio
112+
113+
Description:
114+
MCP server STDIO entrypoint
115+
116+
Usage:
117+
mcp:stdio <jsonrpc_request>
118+
119+
Arguments:
120+
jsonrpc_request JSON-RPC request
121+
```
122+
96123
## Tools
97124

98125
Tools are the core components of the MCP Server Bundle. They allow you to define and manage custom logic that can be triggered by clients.

src/Command/EntrypointCommand.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Command;
6+
7+
use Ecourty\McpServerBundle\Controller\EntrypointController;
8+
use Ecourty\McpServerBundle\EventListener\ExceptionListener;
9+
use Ecourty\McpServerBundle\HttpFoundation\JsonRpcRequest;
10+
use Symfony\Component\Console\Attribute\AsCommand;
11+
use Symfony\Component\Console\Command\Command;
12+
use Symfony\Component\Console\Input\InputArgument;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
use Symfony\Component\Console\Output\OutputInterface;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\RequestStack;
17+
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
18+
use Symfony\Component\HttpKernel\HttpKernelInterface;
19+
use Symfony\Component\HttpKernel\KernelInterface;
20+
use Symfony\Component\Serializer\SerializerInterface;
21+
22+
#[AsCommand(
23+
name: 'mcp:stdio',
24+
description: 'MCP server STDIO entrypoint',
25+
)]
26+
class EntrypointCommand extends Command
27+
{
28+
public function __construct(
29+
private readonly SerializerInterface $serializer,
30+
private readonly EntrypointController $entrypointController,
31+
private readonly ExceptionListener $exceptionListener,
32+
private readonly KernelInterface $kernel,
33+
private readonly RequestStack $requestStack,
34+
) {
35+
parent::__construct();
36+
}
37+
38+
protected function configure(): void
39+
{
40+
$this->addArgument('jsonrpc_request', InputArgument::REQUIRED, 'JSON-RPC request');
41+
}
42+
43+
protected function execute(InputInterface $input, OutputInterface $output): int
44+
{
45+
$jsonRpcRequestString = (string) $input->getArgument('jsonrpc_request');
46+
if (empty($jsonRpcRequestString) === true) {
47+
throw new \UnexpectedValueException('Missing request argument');
48+
}
49+
50+
$jsonRpcRequest = $this->serializer->deserialize($jsonRpcRequestString, JsonRpcRequest::class, 'json');
51+
$request = new Request(content: $jsonRpcRequestString);
52+
53+
try {
54+
$response = $this->entrypointController->__invoke($jsonRpcRequest, $request);
55+
} catch (\Throwable $exception) {
56+
$this->requestStack->push($request);
57+
$exceptionEvent = new ExceptionEvent(
58+
kernel: $this->kernel,
59+
request: $request,
60+
requestType: HttpKernelInterface::MAIN_REQUEST,
61+
e: $exception,
62+
);
63+
$this->exceptionListener->onKernelException($exceptionEvent);
64+
65+
$response = $exceptionEvent->getResponse();
66+
}
67+
68+
if ($response === null) {
69+
throw new \RuntimeException('No response received');
70+
}
71+
72+
$output->write((string) $response->getContent());
73+
74+
return self::SUCCESS;
75+
}
76+
}

src/EventListener/ExceptionListener.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
use Ecourty\McpServerBundle\Enum\McpErrorCode;
88
use Ecourty\McpServerBundle\Exception\MethodHandlerNotFoundException;
99
use Ecourty\McpServerBundle\Exception\RequestHandlingException;
10+
use Ecourty\McpServerBundle\Exception\ToolNotFoundException;
1011
use Ecourty\McpServerBundle\Service\ResponseFactory;
1112
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
13+
use Symfony\Component\HttpFoundation\JsonResponse;
1214
use Symfony\Component\HttpFoundation\RequestStack;
1315
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
1416
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
@@ -38,13 +40,28 @@ public function onKernelException(ExceptionEvent $event): void
3840

3941
$response = match (true) {
4042
$exception instanceof UnprocessableEntityHttpException => $this->responseFactory->error($jsonRpcRequestId, McpErrorCode::PARSE_ERROR),
41-
$exception instanceof MethodHandlerNotFoundException => $this->responseFactory->error($jsonRpcRequestId, McpErrorCode::TOOL_NOT_FOUND),
42-
$exception instanceof RequestHandlingException => $this->responseFactory->error($jsonRpcRequestId, McpErrorCode::INTERNAL_ERROR),
43+
$exception instanceof MethodHandlerNotFoundException => $this->responseFactory->error($jsonRpcRequestId, McpErrorCode::METHOD_NOT_FOUND),
44+
$exception instanceof RequestHandlingException => $this->handleRequestHandlingException($exception, $jsonRpcRequestId),
4345
default => null,
4446
};
4547

4648
if ($response !== null) {
4749
$event->setResponse($response);
4850
}
4951
}
52+
53+
private function handleRequestHandlingException(
54+
RequestHandlingException $exception,
55+
string|int|null $jsonRpcRequestId = null,
56+
): JsonResponse {
57+
$previous = $exception->getPrevious();
58+
if ($previous instanceof ToolNotFoundException === true) {
59+
return $this->responseFactory->error(
60+
id: $jsonRpcRequestId,
61+
errorCode: McpErrorCode::TOOL_NOT_FOUND,
62+
);
63+
}
64+
65+
return $this->responseFactory->error($jsonRpcRequestId, McpErrorCode::INTERNAL_ERROR);
66+
}
5067
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Exception;
6+
7+
class ToolNotFoundException extends \Exception
8+
{
9+
}

src/MethodHandler/ToolsCallMethodHandler.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Ecourty\McpServerBundle\Event\ToolCallExceptionEvent;
1111
use Ecourty\McpServerBundle\Event\ToolResultEvent;
1212
use Ecourty\McpServerBundle\Exception\ToolCallException;
13+
use Ecourty\McpServerBundle\Exception\ToolNotFoundException;
1314
use Ecourty\McpServerBundle\HttpFoundation\JsonRpcRequest;
1415
use Ecourty\McpServerBundle\IO\ToolResult;
1516
use Ecourty\McpServerBundle\Service\InputSanitizer;
@@ -53,7 +54,7 @@ public function handle(JsonRpcRequest $request): array
5354
$toolDefinition = $this->toolRegistry->getToolDefinition($toolName);
5455

5556
if ($tool === null) {
56-
throw new \InvalidArgumentException(\sprintf('Tool "%s" not found.', $request->params['name']));
57+
throw new ToolNotFoundException(\sprintf('Tool "%s" not found.', $request->params['name']));
5758
}
5859

5960
if ($toolDefinition === null) {

0 commit comments

Comments
 (0)