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

Commit b9cfd42

Browse files
Merge pull request #8 from EdouardCourty/feat/support-prompts
feat(prompts): add prompt capabilities
2 parents 83e9dc3 + 65b3b44 commit b9cfd42

44 files changed

Lines changed: 1572 additions & 24 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Attribute/AsPrompt.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Attribute;
6+
7+
use Ecourty\McpServerBundle\Prompt\Argument;
8+
9+
/**
10+
* This attribute is used to mark a class as a prompt for the MCP Server.
11+
*/
12+
#[\Attribute(\Attribute::TARGET_CLASS)]
13+
class AsPrompt
14+
{
15+
private readonly array $arguments;
16+
17+
/**
18+
* @param string $name The name of the prompt.
19+
* @param string|null $description A description of the prompt.
20+
* @param array<Argument|mixed> $arguments An array of argument definitions for the prompt.
21+
*/
22+
public function __construct(
23+
public readonly string $name,
24+
public readonly ?string $description = null,
25+
array $arguments = [],
26+
) {
27+
foreach ($arguments as $argument) {
28+
if ($argument instanceof Argument === false) {
29+
throw new \InvalidArgumentException('All arguments must be instances of ArgumentDefinition.');
30+
}
31+
}
32+
33+
$this->arguments = $arguments;
34+
}
35+
36+
/**
37+
* @return Argument[]
38+
*/
39+
public function getArguments(): array
40+
{
41+
return $this->arguments;
42+
}
43+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Command;
6+
7+
use Ecourty\McpServerBundle\Prompt\Argument;
8+
use Ecourty\McpServerBundle\Prompt\PromptDefinition;
9+
use Ecourty\McpServerBundle\Service\PromptRegistry;
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\Console\Style\SymfonyStyle;
16+
17+
/**
18+
* Command to display information about MCP prompts.
19+
*
20+
* This command allows users to view details about specific MCP prompts or all available prompts.
21+
* It provides a table format for easy readability of prompts attributes such as name, description, and arguments.
22+
*/
23+
#[AsCommand(
24+
name: 'debug:mcp-prompts',
25+
description: 'Display current MCP prompts',
26+
)]
27+
class DebugMcpPromptsCommand extends Command
28+
{
29+
public function __construct(
30+
private readonly PromptRegistry $promptRegistry,
31+
) {
32+
parent::__construct();
33+
}
34+
35+
protected function configure(): void
36+
{
37+
$this
38+
->addArgument('prompt', InputArgument::OPTIONAL, 'Prompt name');
39+
}
40+
41+
protected function execute(InputInterface $input, OutputInterface $output): int
42+
{
43+
$io = new SymfonyStyle($input, $output);
44+
45+
$promptName = $input->getArgument('prompt');
46+
47+
if ($promptName !== null) {
48+
return $this->displaySinglePromptInformation($io, $promptName);
49+
}
50+
51+
return $this->displayAllPromptsInformation($io);
52+
}
53+
54+
private function displaySinglePromptInformation(SymfonyStyle $io, string $promptName): int
55+
{
56+
$prompt = $this->promptRegistry->getPromptDefinition($promptName);
57+
58+
if ($prompt === null) {
59+
$io->error(\sprintf('Prompt "%s" not found.', $promptName));
60+
61+
return self::FAILURE;
62+
}
63+
64+
$io->table(
65+
['Name', 'Description', 'Arguments'],
66+
[
67+
[
68+
$prompt->name,
69+
$prompt->description,
70+
implode(', ', array_map(fn (Argument $argument) => $argument->name, $prompt->arguments ?? [])),
71+
],
72+
],
73+
);
74+
75+
return self::SUCCESS;
76+
}
77+
78+
private function displayAllPromptsInformation(SymfonyStyle $io): int
79+
{
80+
$io->title('MCP Prompts Debug Information');
81+
82+
$promptsDefinitions = $this->promptRegistry->getPromptsDefinitions();
83+
84+
if (empty($promptsDefinitions) === true) {
85+
$io->warning('No prompts found.');
86+
87+
return self::SUCCESS;
88+
}
89+
90+
$io->table(
91+
['Name', 'Description', 'Arguments'],
92+
array_map(static function (PromptDefinition $prompt) {
93+
return [
94+
$prompt->name,
95+
$prompt->description,
96+
implode(', ', array_map(fn (Argument $argument) => $argument->name, $prompt->arguments ?? [])),
97+
];
98+
}, $promptsDefinitions),
99+
);
100+
101+
return self::SUCCESS;
102+
}
103+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Contract\Prompt;
6+
7+
/**
8+
* Represents the content of a prompt message.
9+
*/
10+
interface PromptMessageContentInterface
11+
{
12+
public function toArray(): array;
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Contract;
6+
7+
/**
8+
* Represents a result from a prompt execution.
9+
*/
10+
interface PromptResultInterface
11+
{
12+
public function toArray(): array;
13+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\DependencyInjection\CompilerPass;
6+
7+
use Ecourty\McpServerBundle\Attribute\AsPrompt;
8+
use Ecourty\McpServerBundle\IO\Prompt\PromptResult;
9+
use Ecourty\McpServerBundle\Prompt\Argument;
10+
use Ecourty\McpServerBundle\Service\PromptRegistry;
11+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
12+
use Symfony\Component\DependencyInjection\ContainerBuilder;
13+
14+
/**
15+
* PromptPass is a compiler pass that processes MCP server prompts.
16+
*
17+
* It validates the class and method signatures.
18+
*/
19+
class PromptPass implements CompilerPassInterface
20+
{
21+
public function process(ContainerBuilder $container): void
22+
{
23+
$promptRegistry = $container->getDefinition(PromptRegistry::class);
24+
25+
foreach ($container->getDefinitions() as $definition) {
26+
$class = $definition->getClass();
27+
28+
try {
29+
if ($class === null || class_exists($class) === false) {
30+
continue;
31+
}
32+
} catch (\Throwable) {
33+
continue;
34+
}
35+
36+
$refClass = new \ReflectionClass($class);
37+
38+
$attributes = $refClass->getAttributes(AsPrompt::class);
39+
if (\count($attributes) === 0) {
40+
continue;
41+
}
42+
43+
if (\count($attributes) > 1) {
44+
throw new \LogicException(\sprintf(
45+
'Multiple AsPrompt attributes found on class "%s". Only one is allowed.',
46+
$class,
47+
));
48+
}
49+
50+
if ($refClass->hasMethod('__invoke') === false) {
51+
throw new \LogicException(\sprintf(
52+
'Class "%s" must implement the __invoke method to be used as a prompt.',
53+
$class,
54+
));
55+
}
56+
57+
$invokeMethodParameters = $refClass->getMethod('__invoke')->getParameters();
58+
if (\count($invokeMethodParameters) !== 1) {
59+
throw new \LogicException(\sprintf(
60+
'Class "%s" must have exactly one parameter in the __invoke method to be used as a prompt.',
61+
$class,
62+
));
63+
}
64+
65+
$returnType = $refClass->getMethod('__invoke')->getReturnType();
66+
if ($returnType?->getName() !== PromptResult::class) { // @phpstan-ignore method.notFound
67+
throw new \LogicException(\sprintf(
68+
'The __invoke method in class "%s" must return an instance of %s.',
69+
$class,
70+
PromptResult::class,
71+
));
72+
}
73+
74+
/** @var AsPrompt|null $attr */
75+
$attr = $attributes[0]->newInstance();
76+
if ($attr === null) {
77+
throw new \LogicException(\sprintf(
78+
'Failed to instantiate AsPrompt attribute for class "%s".',
79+
$class,
80+
));
81+
}
82+
83+
$definition
84+
->addTag(name: 'mcp_server.prompt', attributes: [
85+
'name' => $attr->name,
86+
]);
87+
88+
$promptRegistry->addMethodCall(method: 'addPromptDefinition', arguments: [
89+
$attr->name,
90+
$attr->description,
91+
array_map(fn (Argument $argument) => $argument->toArray(), $attr->getArguments()),
92+
]);
93+
}
94+
}
95+
}

src/DependencyInjection/Configuration.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
99
use Symfony\Component\Config\Definition\ConfigurationInterface;
1010

11+
/**
12+
* Configuration class for the MCP Server bundle.
13+
*/
1114
class Configuration implements ConfigurationInterface
1215
{
1316
private const string DEFAULT_NAME = 'MCP Server';

src/DependencyInjection/McpServerBundleExtension.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
use Symfony\Component\DependencyInjection\Extension\Extension;
1212
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
1313

14+
/**
15+
* This class loads and manages the bundle configuration.
16+
*/
1417
class McpServerBundleExtension extends Extension
1518
{
1619
public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface

src/Enum/PromptRole.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Enum;
6+
7+
/**
8+
* Enum representing the roles in a prompt.
9+
*
10+
* This enum defines the different roles that can be used in a prompt,
11+
* such as system, user, and assistant.
12+
*/
13+
enum PromptRole: string
14+
{
15+
case SYSTEM = 'system';
16+
case USER = 'user';
17+
case ASSISTANT = 'assistant';
18+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Event\Prompt;
6+
7+
/**
8+
* Foundation class for prompt events.
9+
*/
10+
abstract class AbstractPromptEvent
11+
{
12+
public function __construct(
13+
private readonly string $promptName,
14+
) {
15+
}
16+
17+
public function getPromptName(): string
18+
{
19+
return $this->promptName;
20+
}
21+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Ecourty\McpServerBundle\Event\Prompt;
6+
7+
use Ecourty\McpServerBundle\Prompt\ArgumentCollection;
8+
9+
/**
10+
* Event triggered when an exception occurs during the execution of a prompt.
11+
*
12+
* Contains the prompt name, arguments if available, and the exception that was thrown.
13+
*/
14+
class PromptExceptionEvent extends AbstractPromptEvent
15+
{
16+
public function __construct(
17+
string $promptName,
18+
private readonly ?ArgumentCollection $arguments,
19+
private readonly \Throwable $exception,
20+
) {
21+
parent::__construct($promptName);
22+
}
23+
24+
public function getArguments(): ?ArgumentCollection
25+
{
26+
return $this->arguments;
27+
}
28+
29+
public function getException(): \Throwable
30+
{
31+
return $this->exception;
32+
}
33+
}

0 commit comments

Comments
 (0)