Skip to content

Commit bb6983e

Browse files
author
klapaudius
committed
Add tests for ModelPreferences, CodeReviewPrompt, and SamplingMessage
- Added unit tests for `ModelPreferences` covering different initialization scenarios and functionality. - Implemented test cases for `CodeReviewPrompt` focusing on message generation and sampling integration. - Added comprehensive tests for `SamplingMessage` ensuring content handling consistency. - Updated `ResourcesReadHandler` and its test to support `SamplingClient`.
1 parent 0ca15c8 commit bb6983e

21 files changed

+3092
-7
lines changed

src/Resources/config/packages/klp_mcp_server.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,15 @@ klp_mcp_server:
8181
# Register your prompts here
8282
prompts:
8383
- KLP\KlpMcpServer\Services\PromptService\Examples\HelloWorldPrompt
84+
- KLP\KlpMcpServer\Services\PromptService\Examples\CodeReviewPrompt
8485

8586
# Resources List
8687
# https://modelcontextprotocol.io/docs/concepts/resources
8788
resources:
8889
- KLP\KlpMcpServer\Services\ResourceService\Examples\HelloWorldResource
90+
- KLP\KlpMcpServer\Services\ResourceService\Examples\ProjectSummaryResource
8991

9092
resources_templates:
9193
- KLP\KlpMcpServer\Services\ResourceService\Examples\McpDocumentationResource
94+
- KLP\KlpMcpServer\Services\ResourceService\Examples\CodeDocumentationResource
9295
---

src/Server/MCPServer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ public function registerToolRepository(ToolRepository $toolRepository): self
142142
public function registerResourceRepository(ResourceRepository $resourceRepository): self
143143
{
144144
$this->registerRequestHandler(new ResourcesListHandler($resourceRepository));
145-
$this->registerRequestHandler(new ResourcesReadHandler($resourceRepository));
145+
$this->registerRequestHandler(new ResourcesReadHandler($resourceRepository, $this->samplingClient));
146146
$this->capabilities->withResources($resourceRepository->getResourceSchemas());
147147

148148
return $this;
@@ -158,7 +158,7 @@ public function registerResourceRepository(ResourceRepository $resourceRepositor
158158
public function registerPromptRepository(PromptRepository $promptRepository): self
159159
{
160160
$this->registerRequestHandler(new PromptsListHandler($promptRepository));
161-
$this->registerRequestHandler(new PromptsGetHandler($promptRepository));
161+
$this->registerRequestHandler(new PromptsGetHandler($promptRepository, $this->samplingClient));
162162
$this->capabilities->withPrompts($promptRepository->getPromptSchemas());
163163

164164
return $this;

src/Server/Request/PromptsGetHandler.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66
use KLP\KlpMcpServer\Exceptions\JsonRpcErrorException;
77
use KLP\KlpMcpServer\Protocol\Handlers\RequestHandler;
88
use KLP\KlpMcpServer\Services\PromptService\PromptRepository;
9+
use KLP\KlpMcpServer\Services\PromptService\SamplingAwarePromptInterface;
10+
use KLP\KlpMcpServer\Services\SamplingService\SamplingClient;
911

1012
class PromptsGetHandler implements RequestHandler
1113
{
1214
private PromptRepository $promptRepository;
1315

14-
public function __construct(PromptRepository $promptRepository)
16+
private ?SamplingClient $samplingClient;
17+
18+
public function __construct(PromptRepository $promptRepository, ?SamplingClient $samplingClient)
1519
{
1620
$this->promptRepository = $promptRepository;
21+
$this->samplingClient = $samplingClient;
1722
}
1823

1924
public function isHandle(string $method): bool
@@ -41,6 +46,12 @@ public function execute(string $method, string $clientId, string|int $messageId,
4146

4247
$arguments = $params['arguments'] ?? [];
4348

49+
// Inject sampling client if the prompt supports it
50+
if ($prompt instanceof SamplingAwarePromptInterface && $this->samplingClient !== null) {
51+
$this->samplingClient->setCurrentClientId($clientId);
52+
$prompt->setSamplingClient($this->samplingClient);
53+
}
54+
4455
return [
4556
'description' => $prompt->getDescription(),
4657
'messages' => $prompt->getMessages($arguments)->getSanitizedMessages(),

src/Server/Request/ResourcesReadHandler.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
use KLP\KlpMcpServer\Protocol\Handlers\RequestHandler;
66
use KLP\KlpMcpServer\Services\ResourceService\ResourceInterface;
77
use KLP\KlpMcpServer\Services\ResourceService\ResourceRepository;
8+
use KLP\KlpMcpServer\Services\ResourceService\SamplingAwareResourceInterface;
9+
use KLP\KlpMcpServer\Services\SamplingService\SamplingClient;
810

911
class ResourcesReadHandler implements RequestHandler
1012
{
1113
private const TEXT_MIME_TYPES = [
1214
// Supported text-based MIME types
15+
'text/markdown',
1316
'text/plain',
1417
'text/html',
1518
'text/css',
@@ -26,9 +29,12 @@ class ResourcesReadHandler implements RequestHandler
2629

2730
private ResourceRepository $resourceRepository;
2831

29-
public function __construct(ResourceRepository $resourceRepository)
32+
private ?SamplingClient $samplingClient;
33+
34+
public function __construct(ResourceRepository $resourceRepository, ?SamplingClient $samplingClient)
3035
{
3136
$this->resourceRepository = $resourceRepository;
37+
$this->samplingClient = $samplingClient;
3238
}
3339

3440
public function isHandle(string $method): bool
@@ -53,6 +59,12 @@ public function execute(string $method, string $clientId, string|int $messageId,
5359
return [];
5460
}
5561

62+
// Inject sampling client if the resource supports it
63+
if ($resource instanceof SamplingAwareResourceInterface && $this->samplingClient !== null) {
64+
$this->samplingClient->setCurrentClientId($clientId);
65+
$resource->setSamplingClient($this->samplingClient);
66+
}
67+
5668
return [
5769
'contents' => [
5870
[
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace KLP\KlpMcpServer\Services\PromptService\Examples;
6+
7+
use KLP\KlpMcpServer\Services\PromptService\Message\CollectionPromptMessage;
8+
use KLP\KlpMcpServer\Services\PromptService\Message\PromptMessageInterface;
9+
use KLP\KlpMcpServer\Services\PromptService\Message\TextPromptMessage;
10+
use KLP\KlpMcpServer\Services\PromptService\SamplingAwarePromptInterface;
11+
use KLP\KlpMcpServer\Services\SamplingService\ModelPreferences;
12+
use KLP\KlpMcpServer\Services\SamplingService\SamplingClient;
13+
14+
/**
15+
* Example prompt that uses sampling to generate dynamic code review prompts.
16+
*
17+
* This prompt demonstrates how to use the sampling feature to create
18+
* context-aware prompts based on the code being reviewed.
19+
*/
20+
class CodeReviewPrompt implements SamplingAwarePromptInterface
21+
{
22+
private ?SamplingClient $samplingClient = null;
23+
24+
public function getName(): string
25+
{
26+
return 'code-review';
27+
}
28+
29+
public function getDescription(): string
30+
{
31+
return 'Generates a comprehensive code review prompt with context-specific questions';
32+
}
33+
34+
public function getArguments(): array
35+
{
36+
return [
37+
[
38+
'name' => 'code',
39+
'description' => 'The code to review',
40+
'required' => true,
41+
],
42+
[
43+
'name' => 'language',
44+
'description' => 'The programming language of the code',
45+
'required' => false,
46+
],
47+
[
48+
'name' => 'focus_areas',
49+
'description' => 'Comma-separated list of areas to focus on (e.g., security,performance,style)',
50+
'required' => false,
51+
],
52+
];
53+
}
54+
55+
public function getMessages(array $arguments = []): CollectionPromptMessage
56+
{
57+
$code = $arguments['code'] ?? '';
58+
$language = $arguments['language'] ?? 'unknown';
59+
$focusAreas = $arguments['focus_areas'] ?? 'general';
60+
61+
$collection = new CollectionPromptMessage();
62+
63+
// Add the system message
64+
$collection->addMessage(
65+
new TextPromptMessage(
66+
PromptMessageInterface::ROLE_ASSISTANT,
67+
$this->getSystemPrompt($language, $focusAreas)
68+
)
69+
);
70+
71+
// If we have sampling capabilities, generate dynamic questions
72+
if ($this->samplingClient !== null && $this->samplingClient->canSample() && !empty($code)) {
73+
$dynamicQuestions = $this->generateDynamicQuestions($code, $language);
74+
if ($dynamicQuestions !== null) {
75+
$collection->addMessage(
76+
new TextPromptMessage(
77+
PromptMessageInterface::ROLE_USER,
78+
$dynamicQuestions
79+
)
80+
);
81+
}
82+
}
83+
84+
// Add the main code review request
85+
$collection->addMessage(
86+
new TextPromptMessage(
87+
PromptMessageInterface::ROLE_USER,
88+
$this->getMainReviewPrompt($code, $focusAreas)
89+
)
90+
);
91+
92+
return $collection;
93+
}
94+
95+
private function getSystemPrompt(string $language, string $focusAreas): string
96+
{
97+
return sprintf(
98+
"You are an expert %s code reviewer. Your task is to provide a thorough code review focusing on: %s. " .
99+
"Be specific in your feedback, provide examples when suggesting improvements, and explain the reasoning behind your recommendations.",
100+
$language !== 'unknown' ? $language : 'software',
101+
$focusAreas
102+
);
103+
}
104+
105+
private function getMainReviewPrompt(string $code, string $focusAreas): string
106+
{
107+
$areas = array_map('trim', explode(',', $focusAreas));
108+
$areaPrompts = [];
109+
110+
foreach ($areas as $area) {
111+
switch (strtolower($area)) {
112+
case 'security':
113+
$areaPrompts[] = "- Security vulnerabilities and potential attack vectors";
114+
break;
115+
case 'performance':
116+
$areaPrompts[] = "- Performance bottlenecks and optimization opportunities";
117+
break;
118+
case 'style':
119+
$areaPrompts[] = "- Code style, naming conventions, and formatting";
120+
break;
121+
case 'testing':
122+
$areaPrompts[] = "- Test coverage and testability of the code";
123+
break;
124+
case 'architecture':
125+
$areaPrompts[] = "- Architectural patterns and design decisions";
126+
break;
127+
default:
128+
$areaPrompts[] = "- General code quality and best practices";
129+
}
130+
}
131+
132+
return sprintf(
133+
"Please review the following code:\n\n```\n%s\n```\n\nFocus your review on:\n%s\n\n" .
134+
"Provide specific, actionable feedback with examples where appropriate.",
135+
$code,
136+
implode("\n", $areaPrompts)
137+
);
138+
}
139+
140+
private function generateDynamicQuestions(string $code, string $language): string|null
141+
{
142+
try {
143+
// Use sampling to analyze the code and generate specific questions
144+
$prompt = sprintf(
145+
"Analyze this %s code and generate 3-5 specific, insightful questions that a code reviewer should ask. " .
146+
"Focus on potential issues, design decisions, or areas that need clarification:\n\n```\n%s\n```\n\n" .
147+
"Format your response as a bulleted list of questions only.",
148+
$language,
149+
$code
150+
);
151+
152+
$response = $this->samplingClient->createTextRequest(
153+
$prompt,
154+
new ModelPreferences(
155+
hints: [['name' => 'claude-3-haiku']],
156+
intelligencePriority: 0.5,
157+
speedPriority: 0.8
158+
),
159+
null,
160+
500
161+
);
162+
163+
$questions = $response->getContent()->getText();
164+
if (!empty($questions)) {
165+
return "Additionally, please address these specific questions:\n\n" . $questions;
166+
}
167+
} catch (\Exception $e) {
168+
// If sampling fails, we'll just skip the dynamic questions
169+
// and use the static prompt
170+
}
171+
172+
return null;
173+
}
174+
175+
public function setSamplingClient(SamplingClient $samplingClient): void
176+
{
177+
$this->samplingClient = $samplingClient;
178+
}
179+
}

0 commit comments

Comments
 (0)