Skip to content

Commit 3db32e1

Browse files
committed
Add attribute-based metadata system for MCP Server components
This commit introduces an attribute-based metadata system for defining MCP Server components (prompts, resources, and tools). This approach replaces hardcoded lists with self-documenting metadata directly on implementing classes.
1 parent ad9045b commit 3db32e1

27 files changed

+692
-258
lines changed

context-generator

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use Butschster\ContextGenerator\Lib\HttpClient\HttpClientFactory;
2222
use Butschster\ContextGenerator\Lib\HttpClient\HttpClientInterface;
2323
use Butschster\ContextGenerator\Lib\Logger\ConsoleLogger;
2424
use Butschster\ContextGenerator\Lib\Logger\LoggerFactory;
25+
use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry;
2526
use Butschster\ContextGenerator\McpServer\Routing\McpResponseStrategy;
2627
use Butschster\ContextGenerator\McpServer\Routing\RouteRegistrar;
2728
use Butschster\ContextGenerator\Modifier\SourceModifierRegistry;
@@ -166,6 +167,7 @@ $container->bindSingleton(Router::class, static function (StrategyInterface $str
166167
return $router;
167168
});
168169
$container->bindSingleton(RouteRegistrar::class, RouteRegistrar::class);
170+
$container->bindSingleton(McpItemsRegistry::class, McpItemsRegistry::class);
169171

170172
// Register all commands
171173
$application->add(

src/Console/MCPServerCommand.php

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Butschster\ContextGenerator\DocumentCompilerFactory;
1313
use Butschster\ContextGenerator\McpServer\ServerFactory;
1414
use Monolog\Handler\RotatingFileHandler;
15+
use Monolog\Level;
1516
use Monolog\Logger;
1617
use Spiral\Core\Container;
1718
use Symfony\Component\Console\Attribute\AsCommand;
@@ -34,10 +35,17 @@ public function __construct(
3435
public function __invoke(
3536
DocumentCompilerFactory $documentCompilerFactory,
3637
ConfigurationProviderFactory $configurationProviderFactory,
37-
ServerFactory $factory,
3838
): int {
3939
$this->setLogger(new Logger('mcp', [
40-
new RotatingFileHandler($this->dirs->rootPath . '/mcp.log'),
40+
new RotatingFileHandler(
41+
filename: $this->dirs->rootPath . '/mcp.log',
42+
level: match (true) {
43+
$this->output->isVeryVerbose() => Level::Debug,
44+
$this->output->isVerbose() => Level::Info,
45+
$this->output->isQuiet() => Level::Error,
46+
default => Level::Warning,
47+
},
48+
),
4149
]));
4250

4351
$this->logger->info('Starting MCP server...');
@@ -92,7 +100,7 @@ public function __invoke(
92100

93101

94102
// Create and run the MCP server
95-
$server = $factory->create($this->logger);
103+
$server = $this->container->get(ServerFactory::class)->create($this->logger);
96104

97105
$server->run(name: $this->input->getOption('name'));
98106

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Action\Prompts;
6+
7+
use Butschster\ContextGenerator\McpServer\Attribute\Prompt;
8+
use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get;
9+
use Mcp\Types\GetPromptResult;
10+
use Mcp\Types\PromptMessage;
11+
use Mcp\Types\Role;
12+
use Mcp\Types\TextContent;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
use Psr\Log\LoggerInterface;
15+
16+
#[Prompt(
17+
name: 'available-context',
18+
description: 'Provides a list of available contexts',
19+
)]
20+
final readonly class AvailableContextPromptAction
21+
{
22+
public function __construct(
23+
private LoggerInterface $logger,
24+
) {}
25+
26+
#[Get(path: '/prompt/available-context', name: 'prompts.available-context')]
27+
public function __invoke(ServerRequestInterface $request): GetPromptResult
28+
{
29+
$this->logger->info('Getting available-context prompt');
30+
31+
return new GetPromptResult(
32+
messages: [
33+
new PromptMessage(
34+
role: Role::USER,
35+
content: new TextContent(
36+
text: "Provide list of available contexts in JSON format",
37+
),
38+
),
39+
],
40+
);
41+
}
42+
}

src/McpServer/Action/Prompts/GetPromptAction.php

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/McpServer/Action/Prompts/ListPromptsAction.php

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,29 @@
44

55
namespace Butschster\ContextGenerator\McpServer\Action\Prompts;
66

7+
use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry;
78
use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get;
89
use Mcp\Types\ListPromptsResult;
9-
use Mcp\Types\Prompt;
1010
use Psr\Http\Message\ServerRequestInterface;
1111
use Psr\Log\LoggerInterface;
1212

1313
final readonly class ListPromptsAction
1414
{
1515
public function __construct(
1616
private LoggerInterface $logger,
17+
private McpItemsRegistry $registry,
1718
) {}
1819

1920
#[Get(path: '/prompts/list', name: 'prompts.list')]
2021
public function __invoke(ServerRequestInterface $request): ListPromptsResult
2122
{
2223
$this->logger->info('Listing available prompts');
2324

24-
// Return available prompts in a format that can be converted to ListPromptsResult
25-
return new ListPromptsResult([
26-
new Prompt(
27-
name: 'available-context',
28-
description: 'Provides a list of available contexts',
29-
),
30-
new Prompt(
31-
name: 'project-structure',
32-
description: 'Tries to guess the project structure',
33-
),
34-
]);
25+
$prompts = [];
26+
foreach ($this->registry->getPrompts() as $prompt) {
27+
$prompts[] = $prompt;
28+
}
29+
30+
return new ListPromptsResult($prompts);
3531
}
3632
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Action\Prompts;
6+
7+
use Butschster\ContextGenerator\McpServer\Attribute\Prompt;
8+
use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get;
9+
use Mcp\Types\GetPromptResult;
10+
use Mcp\Types\PromptMessage;
11+
use Mcp\Types\Role;
12+
use Mcp\Types\TextContent;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
use Psr\Log\LoggerInterface;
15+
16+
#[Prompt(
17+
name: 'project-structure',
18+
description: 'Tries to guess the project structure',
19+
)]
20+
final readonly class ProjectStructurePromptAction
21+
{
22+
public function __construct(
23+
private LoggerInterface $logger,
24+
) {}
25+
26+
#[Get(path: '/prompt/project-structure', name: 'prompts.project-structure')]
27+
public function __invoke(ServerRequestInterface $request): GetPromptResult
28+
{
29+
$this->logger->info('Getting project-structure prompt');
30+
31+
return new GetPromptResult(
32+
messages: [
33+
new PromptMessage(
34+
role: Role::USER,
35+
content: new TextContent(
36+
text: "Look at available contexts and try to find the project structure. If there is no context for structure. Request structure from context using JSON schema. Provide the result in JSON format",
37+
),
38+
),
39+
],
40+
);
41+
}
42+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Action\Resources;
6+
7+
use Butschster\ContextGenerator\Directories;
8+
use Butschster\ContextGenerator\FilesInterface;
9+
use Butschster\ContextGenerator\McpServer\Attribute\Resource;
10+
use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get;
11+
use Mcp\Types\ReadResourceResult;
12+
use Mcp\Types\TextResourceContents;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
use Psr\Log\LoggerInterface;
15+
16+
#[Resource(
17+
name: 'Json Schema of context generator',
18+
description: 'Returns a simplified JSON schema of the context generator',
19+
uri: 'ctx://json-schema',
20+
mimeType: 'application/json',
21+
)]
22+
final readonly class JsonSchemaResourceAction
23+
{
24+
public function __construct(
25+
private LoggerInterface $logger,
26+
private FilesInterface $files,
27+
private Directories $dirs,
28+
) {}
29+
30+
#[Get(path: '/resource/ctx/json-schema', name: 'resources.ctx.json-schema')]
31+
public function __invoke(ServerRequestInterface $request): ReadResourceResult
32+
{
33+
$this->logger->info('Getting JSON schema');
34+
35+
return new ReadResourceResult([
36+
new TextResourceContents(
37+
text: $this->getJsonSchema(),
38+
uri: 'ctx://json-schema',
39+
mimeType: 'application/json',
40+
),
41+
]);
42+
}
43+
44+
/**
45+
* Get simplified JSON schema
46+
*/
47+
private function getJsonSchema(): string
48+
{
49+
$schema = \json_decode(
50+
$this->files->read($this->dirs->jsonSchemaPath),
51+
associative: true,
52+
);
53+
54+
unset(
55+
$schema['properties']['import'],
56+
$schema['properties']['settings'],
57+
$schema['definitions']['document']['properties']['modifiers'],
58+
$schema['definitions']['source']['properties']['modifiers'],
59+
$schema['definitions']['urlSource'],
60+
$schema['definitions']['githubSource'],
61+
$schema['definitions']['textSource'],
62+
$schema['definitions']['composerSource'],
63+
$schema['definitions']['php-content-filter'],
64+
$schema['definitions']['php-docs'],
65+
$schema['definitions']['sanitizer'],
66+
$schema['definitions']['modifiers'],
67+
$schema['definitions']['visibilityOptions'],
68+
);
69+
70+
$schema['definitions']['source']['properties']['type']['enum'] = ['file', 'tree', 'git_diff'];
71+
72+
return (string) \json_encode($schema);
73+
}
74+
}

src/McpServer/Action/Resources/ListResourcesAction.php

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Butschster\ContextGenerator\McpServer\Action\Resources;
66

77
use Butschster\ContextGenerator\ConfigLoader\ConfigLoaderInterface;
8+
use Butschster\ContextGenerator\McpServer\Registry\McpItemsRegistry;
89
use Butschster\ContextGenerator\McpServer\Routing\Attribute\Get;
910
use Mcp\Types\ListResourcesResult;
1011
use Mcp\Types\Resource;
@@ -16,29 +17,23 @@
1617
public function __construct(
1718
private LoggerInterface $logger,
1819
private ConfigLoaderInterface $configLoader,
20+
private McpItemsRegistry $registry,
1921
) {}
2022

2123
#[Get(path: '/resources/list', name: 'resources.list')]
2224
public function __invoke(ServerRequestInterface $request): ListResourcesResult
2325
{
2426
$this->logger->info('Listing available resources');
2527

26-
$documents = $this->configLoader->load();
27-
$resources = [
28-
new Resource(
29-
name: 'List of available contexts',
30-
uri: 'ctx://list',
31-
description: 'Returns a list of available contexts of project in document format',
32-
mimeType: 'text/markdown',
33-
),
34-
new Resource(
35-
name: 'Json Schema of context generator',
36-
uri: 'ctx://json-schema',
37-
description: 'Returns a simplified JSON schema of the context generator',
38-
mimeType: 'application/json',
39-
),
40-
];
28+
$resources = [];
29+
30+
// Get resources from registry
31+
foreach ($this->registry->getResources() as $resource) {
32+
$resources[] = $resource;
33+
}
4134

35+
// Add document resources from config loader
36+
$documents = $this->configLoader->load();
4237
foreach ($documents->getItems() as $document) {
4338
$resources[] = new Resource(
4439
name: $document->outputPath,

src/McpServer/Action/Tools/Filesystem/FileInfoAction.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,24 @@
66

77
use Butschster\ContextGenerator\Directories;
88
use Butschster\ContextGenerator\FilesInterface;
9+
use Butschster\ContextGenerator\McpServer\Attribute\InputSchema;
10+
use Butschster\ContextGenerator\McpServer\Attribute\Tool;
911
use Butschster\ContextGenerator\McpServer\Routing\Attribute\Post;
1012
use Mcp\Types\CallToolResult;
1113
use Mcp\Types\TextContent;
1214
use Psr\Http\Message\ServerRequestInterface;
1315
use Psr\Log\LoggerInterface;
1416

17+
#[Tool(
18+
name: 'file-info',
19+
description: 'Get information about a file within the project directory structure',
20+
)]
21+
#[InputSchema(
22+
name: 'path',
23+
type: 'string',
24+
description: 'Path to the file, relative to project root. Only files within project directory can be accessed.',
25+
required: true,
26+
)]
1527
final readonly class FileInfoAction
1628
{
1729
public function __construct(

0 commit comments

Comments
 (0)