Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ All notable changes to `mcp/sdk` will be documented in this file.
* [BC Break] Validate a tool result's `structuredContent` against the tool's `outputSchema`, which the specification requires the server to honour. A mismatch is answered with a `CallToolResult` carrying `isError: true` instead of the non-conforming value, matching the TypeScript, Python and Java SDKs. Skipped when the tool declares no `outputSchema`, when the result carries no `structuredContent`, and when the result is already an error.
* Stop the server `Protocol` from logging full JSON-RPC payloads (tool arguments, client replies) at info level: info records now carry only the method and id, the raw message is logged at debug level.
* Add `PassthroughMiddleware` to opt `StreamableHttpTransport` out of its default middleware without the warning an empty `$middleware` list logs.
* Add `ElicitationSchema::getDefaults()`, returning the declared `default` of each field to accept a form elicitation with.
* [BC Break] `AbstractSchemaDefinition` declares an abstract `getDefault()`, which a custom schema definition has to implement.

0.8.0
-----
Expand Down
5 changes: 5 additions & 0 deletions src/Schema/Elicitation/AbstractSchemaDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ protected function buildBaseJson(string $type): array
return $data;
}

/**
* The value the field is pre-filled with, or null when it declares none.
*/
abstract public function getDefault(): mixed;

/**
* @return array<string, mixed>
*/
Expand Down
5 changes: 5 additions & 0 deletions src/Schema/Elicitation/BooleanSchemaDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public static function fromArray(array $data): self
);
}

public function getDefault(): ?bool
{
return $this->default;
}

/**
* @return array{
* type: string,
Expand Down
18 changes: 18 additions & 0 deletions src/Schema/Elicitation/ElicitationSchema.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,24 @@ private static function resolveArrayType(array $data): AbstractSchemaDefinition
throw new InvalidArgumentException('Array type must have "items" with either "enum" or "anyOf".');
}

/**
* The declared default of each field that has one, keyed by field name.
*
* @return array<string, mixed>
*/
public function getDefaults(): array
{
$defaults = [];

foreach ($this->properties as $name => $property) {
if (null !== $default = $property->getDefault()) {
$defaults[$name] = $default;
}
}

return $defaults;
}

/**
* @return array{
* type: string,
Expand Down
5 changes: 5 additions & 0 deletions src/Schema/Elicitation/EnumSchemaDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ enumNames: $data['enumNames'] ?? null,
);
}

public function getDefault(): ?string
{
return $this->default;
}

/**
* @return array{
* type: string,
Expand Down
8 changes: 8 additions & 0 deletions src/Schema/Elicitation/MultiSelectEnumSchemaDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ enum: $data['items']['enum'],
);
}

/**
* @return string[]|null
*/
public function getDefault(): ?array
{
return $this->default;
}

/**
* @return array<string, mixed>
*/
Expand Down
5 changes: 5 additions & 0 deletions src/Schema/Elicitation/NumberSchemaDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public static function fromArray(array $data): self
);
}

public function getDefault(): int|float|null
{
return $this->default;
}

/**
* @return array{
* type: string,
Expand Down
5 changes: 5 additions & 0 deletions src/Schema/Elicitation/StringSchemaDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ public static function fromArray(array $data): self
);
}

public function getDefault(): ?string
{
return $this->default;
}

/**
* @return array{
* type: string,
Expand Down
5 changes: 5 additions & 0 deletions src/Schema/Elicitation/TitledEnumSchemaDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ public static function fromArray(array $data): self
);
}

public function getDefault(): ?string
{
return $this->default;
}

/**
* @return array<string, mixed>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ public static function fromArray(array $data): self
);
}

/**
* @return string[]|null
*/
public function getDefault(): ?array
{
return $this->default;
}

/**
* @return array<string, mixed>
*/
Expand Down
16 changes: 11 additions & 5 deletions tests/Conformance/client.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,9 @@
->setLogger($logger);

/**
* Accepts every elicitation with an empty payload.
* Accepts every form elicitation with its declared defaults.
*
* Enough for the scenarios here, which check that the client asked and echoed
* correctly rather than what a user would have typed.
* A url-mode elicitation has no schema to fill, so it is declined.
*/
$acceptElicitation = new class($logger) implements RequestHandlerInterface {
public function __construct(private readonly Psr\Log\LoggerInterface $logger)
Expand All @@ -69,9 +68,16 @@ public function supports(Request $request): bool

public function handle(Request $request): Response
{
$this->logger->info('Received elicitation request, accepting with empty content');
if (!$request instanceof ElicitRequest || null === $request->requestedSchema) {
$this->logger->info('Received elicitation request without a schema, declining');

return new Response($request->getId(), new ElicitResult(ElicitAction::Accept, []));
return new Response($request->getId(), new ElicitResult(ElicitAction::Decline));
}

$content = $request->requestedSchema->getDefaults();
$this->logger->info(sprintf('Received elicitation request, accepting with %d defaults', count($content)));

return new Response($request->getId(), new ElicitResult(ElicitAction::Accept, $content));
}
};

Expand Down
52 changes: 52 additions & 0 deletions tests/Unit/Schema/Elicitation/ElicitationSchemaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,58 @@ public function testConstructorWithMultipleTypes(): void
$this->assertInstanceOf(EnumSchemaDefinition::class, $schema->properties['rating']);
}

public function testGetDefaults(): void
{
$schema = ElicitationSchema::fromArray([
'type' => 'object',
'properties' => [
'name' => ['type' => 'string', 'title' => 'Name', 'default' => 'Ada'],
'age' => ['type' => 'integer', 'title' => 'Age', 'default' => 0],
'score' => ['type' => 'number', 'title' => 'Score', 'default' => 95.5],
'color' => ['type' => 'string', 'title' => 'Color', 'enum' => ['red', 'blue'], 'default' => 'blue'],
'plan' => [
'type' => 'string',
'title' => 'Plan',
'oneOf' => [
['const' => 'free', 'title' => 'Free'],
['const' => 'pro', 'title' => 'Pro'],
],
'default' => 'pro',
],
'subscribe' => ['type' => 'boolean', 'title' => 'Subscribe', 'default' => false],
'tags' => [
'type' => 'array',
'title' => 'Tags',
'items' => ['type' => 'string', 'enum' => ['php', 'mcp']],
'default' => ['php'],
],
'channels' => [
'type' => 'array',
'title' => 'Channels',
'items' => [
'anyOf' => [
['const' => 'mail', 'title' => 'Mail'],
['const' => 'sms', 'title' => 'SMS'],
],
],
'default' => ['mail', 'sms'],
],
'withoutDefault' => ['type' => 'string', 'title' => 'Without default'],
],
]);

$this->assertSame([
'name' => 'Ada',
'age' => 0,
'score' => 95.5,
'color' => 'blue',
'plan' => 'pro',
'subscribe' => false,
'tags' => ['php'],
'channels' => ['mail', 'sms'],
], $schema->getDefaults());
}

public function testConstructorWithEmptyProperties(): void
{
$this->expectException(InvalidArgumentException::class);
Expand Down
Loading