Skip to content

Commit ea042a6

Browse files
committed
feat(mcp): implement template-based prompt extension system
- Add support for template-based prompts that can be extended - Implement variable substitution in templates using existing variable system - Allow templates to extend other templates (inheritance chain) - Add proper error handling for circular dependencies or missing templates - Maintain backward compatibility with existing prompts Closes #180
1 parent 8a0522d commit ea042a6

18 files changed

+649
-46
lines changed
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
prompts:
2+
# Template for issues
3+
- id: template-issue
4+
description: Template for creating issues
5+
type: template
6+
messages:
7+
- role: user
8+
content: "Create a new issue with the following title and description: {{title}} {{description}}"
9+
10+
# Template for bug issues, extending the base issue template
11+
- id: bug-issue
12+
description: Create a new bug issue
13+
type: prompt
14+
extend:
15+
- id: template-issue
16+
arguments:
17+
title: 'Bug: {{title}}'
18+
description: '{{description}}'
19+
schema:
20+
properties:
21+
title:
22+
description: The title of the bug
23+
description:
24+
description: The description of the bug
25+
required:
26+
- title
27+
- description
28+
29+
# Template for feature issues, extending the base issue template
30+
- id: feature-issue
31+
description: Create a new feature issue
32+
type: prompt
33+
extend:
34+
- id: template-issue
35+
arguments:
36+
title: 'Feature: {{title}}'
37+
description: '{{description}}'
38+
schema:
39+
properties:
40+
title:
41+
description: The title of the feature
42+
description:
43+
description: The description of the feature
44+
required:
45+
- title
46+
- description
47+
48+
# More complex template example, extending another template
49+
- id: template-complex-issue
50+
type: template
51+
description: Template for complex issues with priority
52+
extend:
53+
- id: template-issue
54+
arguments:
55+
title: '{{type}}: {{title}}'
56+
description: '{{description}} \n\n**Priority**: {{priority}}'
57+
58+
# Priority bug issue using the complex template
59+
- id: priority-bug-issue
60+
description: Create a new priority bug issue
61+
type: prompt
62+
extend:
63+
- id: template-complex-issue
64+
arguments:
65+
type: 'Bug'
66+
priority: 'High'
67+
schema:
68+
properties:
69+
title:
70+
description: The title of the bug
71+
description:
72+
description: The description of the bug
73+
required:
74+
- title
75+
- description

json-schema.json

+99-11
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,10 @@
269269
},
270270
"type": {
271271
"type": "string",
272-
"enum": ["run", "http"],
272+
"enum": [
273+
"run",
274+
"http"
275+
],
273276
"description": "Type of tool (run = command execution, http = HTTP requests)",
274277
"default": "run"
275278
},
@@ -309,7 +312,9 @@
309312
}
310313
},
311314
"then": {
312-
"required": ["commands"]
315+
"required": [
316+
"commands"
317+
]
313318
}
314319
},
315320
{
@@ -321,22 +326,34 @@
321326
}
322327
},
323328
"then": {
324-
"required": ["requests"]
329+
"required": [
330+
"requests"
331+
]
325332
}
326333
}
327334
]
328335
},
329336
"httpRequest": {
330337
"type": "object",
331-
"required": ["url"],
338+
"required": [
339+
"url"
340+
],
332341
"properties": {
333342
"url": {
334343
"type": "string",
335344
"description": "URL to send the request to, may contain {{argument}} placeholders"
336345
},
337346
"method": {
338347
"type": "string",
339-
"enum": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
348+
"enum": [
349+
"GET",
350+
"POST",
351+
"PUT",
352+
"DELETE",
353+
"PATCH",
354+
"HEAD",
355+
"OPTIONS"
356+
],
340357
"description": "HTTP method to use",
341358
"default": "GET"
342359
},
@@ -423,8 +440,7 @@
423440
"prompt": {
424441
"type": "object",
425442
"required": [
426-
"id",
427-
"messages"
443+
"id"
428444
],
429445
"properties": {
430446
"id": {
@@ -435,6 +451,15 @@
435451
"type": "string",
436452
"description": "Human-readable description of the prompt"
437453
},
454+
"type": {
455+
"type": "string",
456+
"enum": [
457+
"prompt",
458+
"template"
459+
],
460+
"description": "Type of prompt (regular prompt or template)",
461+
"default": "prompt"
462+
},
438463
"schema": {
439464
"$ref": "#/definitions/inputSchema",
440465
"description": "Defines input parameters for this prompt"
@@ -459,13 +484,74 @@
459484
},
460485
"content": {
461486
"type": "string",
462-
"description": "The content of the message, may contain variable placeholders like ${variableName}"
487+
"description": "The content of the message, may contain variable placeholders like ${variableName} or {{variableName}}"
488+
}
489+
}
490+
}
491+
},
492+
"extend": {
493+
"type": "array",
494+
"description": "List of templates to extend",
495+
"items": {
496+
"type": "object",
497+
"required": [
498+
"id"
499+
],
500+
"properties": {
501+
"id": {
502+
"type": "string",
503+
"description": "ID of the template to extend"
504+
},
505+
"arguments": {
506+
"type": "object",
507+
"description": "Arguments to pass to the template",
508+
"additionalProperties": {
509+
"type": "string"
510+
}
511+
}
512+
}
513+
}
514+
}
515+
},
516+
"allOf": [
517+
{
518+
"if": {
519+
"properties": {
520+
"type": {
521+
"const": "prompt"
463522
}
464523
}
465524
},
466-
"minItems": 1
525+
"then": {
526+
"anyOf": [
527+
{
528+
"required": [
529+
"messages"
530+
]
531+
},
532+
{
533+
"required": [
534+
"extend"
535+
]
536+
}
537+
]
538+
}
539+
},
540+
{
541+
"if": {
542+
"properties": {
543+
"type": {
544+
"const": "template"
545+
}
546+
}
547+
},
548+
"then": {
549+
"required": [
550+
"messages"
551+
]
552+
}
467553
}
468-
}
554+
]
469555
},
470556
"document": {
471557
"type": "object",
@@ -1257,7 +1343,9 @@
12571343
"description": "Patterns to include only specific paths"
12581344
},
12591345
"maxFiles": {
1260-
"type": ["integer"],
1346+
"type": [
1347+
"integer"
1348+
],
12611349
"description": "Maximum number of files to include (0 for no limit)",
12621350
"minimum": 0,
12631351
"default": 0

src/Lib/Variable/CompositeProcessor.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* @param VariableReplacementProcessor[] $processors
1111
*/
1212
public function __construct(
13-
public array $processors,
13+
public array $processors = [],
1414
) {}
1515

1616
public function process(string $text): string

src/Lib/Variable/VariableResolver.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
final readonly class VariableResolver
1111
{
1212
public function __construct(
13-
private VariableReplacementProcessorInterface $processor,
13+
private VariableReplacementProcessorInterface $processor = new CompositeProcessor(),
1414
) {}
1515

1616
public function with(VariableReplacementProcessorInterface $processor): self

src/McpServer/Prompt/Exception/PromptParsingException.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
/**
88
* Exception thrown when a prompt configuration cannot be parsed.
99
*/
10-
final class PromptParsingException extends \RuntimeException
10+
class PromptParsingException extends \RuntimeException
1111
{
1212
// No additional methods needed, this is just a specialized exception type
1313
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Prompt\Exception;
6+
7+
/**
8+
* Exception thrown when a template resolution fails.
9+
*/
10+
final class TemplateResolutionException extends PromptParsingException
11+
{
12+
// No additional methods needed, this is just a specialized exception type
13+
}

src/McpServer/Prompt/PromptDefinition.php renamed to src/McpServer/Prompt/Extension/PromptDefinition.php

+28-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
declare(strict_types=1);
44

5-
namespace Butschster\ContextGenerator\McpServer\Prompt;
5+
namespace Butschster\ContextGenerator\McpServer\Prompt\Extension;
66

7+
use Butschster\ContextGenerator\McpServer\Prompt\PromptType;
78
use Mcp\Types\Prompt;
89
use Mcp\Types\PromptMessage;
910

@@ -14,6 +15,9 @@ public function __construct(
1415
public Prompt $prompt,
1516
/** @var PromptMessage[] */
1617
public array $messages = [],
18+
public PromptType $type = PromptType::Prompt,
19+
/** @var PromptExtension[] */
20+
public array $extensions = [],
1721
) {}
1822

1923
public function jsonSerialize(): array
@@ -35,9 +39,32 @@ public function jsonSerialize(): array
3539

3640
return \array_filter([
3741
'id' => $this->id,
42+
'type' => $this->type->value,
3843
'description' => $this->prompt->description,
3944
'schema' => $schema,
4045
'messages' => $this->messages,
46+
'extend' => $this->serializeExtensions(),
4147
], static fn($value) => $value !== null && $value !== []);
4248
}
49+
50+
/**
51+
* Serializes the extensions for JSON output.
52+
*
53+
* @return array<mixed>|null The serialized extensions or null if empty
54+
*/
55+
private function serializeExtensions(): ?array
56+
{
57+
if (empty($this->extensions)) {
58+
return null;
59+
}
60+
61+
// Convert extensions to the format used in configuration
62+
return \array_map(static function (PromptExtension $ext) {
63+
$args = [];
64+
foreach ($ext->arguments as $arg) {
65+
$args[$arg->name] = $arg->value;
66+
}
67+
return ['id' => $ext->templateId, 'arguments' => $args];
68+
}, $this->extensions);
69+
}
4370
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Butschster\ContextGenerator\McpServer\Prompt\Extension;
6+
7+
/**
8+
* Represents a template extension configuration.
9+
*/
10+
final readonly class PromptExtension
11+
{
12+
/**
13+
* @param string $templateId The ID of the template to extend
14+
* @param PromptExtensionArgument[] $arguments The arguments to pass to the template
15+
*/
16+
public function __construct(
17+
public string $templateId,
18+
public array $arguments = [],
19+
) {}
20+
21+
/**
22+
* Creates a PromptExtension from a configuration array.
23+
*
24+
* @param array<string, mixed> $config The extension configuration
25+
* @return self The created PromptExtension
26+
* @throws \InvalidArgumentException If the configuration is invalid
27+
*/
28+
public static function fromArray(array $config): self
29+
{
30+
if (empty($config['id']) || !\is_string($config['id'])) {
31+
throw new \InvalidArgumentException('Extension must have a template ID');
32+
}
33+
34+
$arguments = [];
35+
if (isset($config['arguments']) && \is_array($config['arguments'])) {
36+
foreach ($config['arguments'] as $name => $value) {
37+
if (!\is_string($name) || !\is_string($value)) {
38+
throw new \InvalidArgumentException(
39+
\sprintf('Extension argument "%s" must have a string value', $name),
40+
);
41+
}
42+
43+
$arguments[] = new PromptExtensionArgument($name, $value);
44+
}
45+
}
46+
47+
return new self($config['id'], $arguments);
48+
}
49+
}

0 commit comments

Comments
 (0)