|
34 | 34 | use Mcp\Server\Transport\Http\StandardPhpAdapter; |
35 | 35 | use Mcp\Types\BlobResourceContents; |
36 | 36 | use Mcp\Types\CallToolResult; |
| 37 | +use Mcp\Types\CompleteResult; |
| 38 | +use Mcp\Types\CompletionContext; |
| 39 | +use Mcp\Types\CompletionObject; |
37 | 40 | use Mcp\Types\GetPromptResult; |
38 | 41 | use Mcp\Types\ListPromptsResult; |
39 | 42 | use Mcp\Types\ListResourcesResult; |
|
42 | 45 | use Mcp\Types\Prompt; |
43 | 46 | use Mcp\Types\PromptArgument; |
44 | 47 | use Mcp\Types\PromptMessage; |
| 48 | +use Mcp\Types\PromptReference; |
45 | 49 | use Mcp\Types\ReadResourceResult; |
46 | 50 | use Mcp\Types\Resource; |
| 51 | +use Mcp\Types\ResourceReference; |
47 | 52 | use Mcp\Types\ResourceTemplate; |
48 | 53 | use Mcp\Types\Role; |
49 | 54 | use Mcp\Types\Task; |
@@ -127,6 +132,15 @@ class McpServer |
127 | 132 | */ |
128 | 133 | protected array $resourceTemplateHandlers = []; |
129 | 134 |
|
| 135 | + /** @var array<string, callable> Prompt-argument completion providers, keyed "promptName\0argName". */ |
| 136 | + protected array $promptCompletionProviders = []; |
| 137 | + |
| 138 | + /** @var array<string, callable> Resource-template completion providers, keyed "uriTemplate\0argName". */ |
| 139 | + protected array $resourceTemplateCompletionProviders = []; |
| 140 | + |
| 141 | + /** @var bool Whether the completion/complete handler has been registered. */ |
| 142 | + protected bool $completionHandlerRegistered = false; |
| 143 | + |
130 | 144 | /** @var array<string, mixed> [Added] HTTP transport options. */ |
131 | 145 | protected array $httpOptions = []; |
132 | 146 |
|
@@ -563,6 +577,62 @@ private function normalizeReadResourceResult(mixed $result, string $uri, string |
563 | 577 | throw McpServerException::invalidResourceResult($result); |
564 | 578 | } |
565 | 579 |
|
| 580 | + /** |
| 581 | + * Register a completion provider for a prompt argument. |
| 582 | + * |
| 583 | + * The provider supplies autocomplete suggestions as the user types a value |
| 584 | + * for the named argument of the named prompt. Registering any completion |
| 585 | + * provider causes the server to advertise the `completions` capability. |
| 586 | + * |
| 587 | + * The provider is called as `$provider(string $value, array $context = [])`: |
| 588 | + * - `$value` is the partial argument value typed so far. |
| 589 | + * - `$context` is the map of already-resolved argument values the client |
| 590 | + * sent (empty when none) — useful to filter on a prior selection. A |
| 591 | + * provider that doesn't need context simply omits the second parameter. |
| 592 | + * |
| 593 | + * It may return a `string[]` (auto-wrapped, truncated to 100 with |
| 594 | + * `hasMore`/`total` if longer), a {@see CompletionObject}, or a |
| 595 | + * {@see CompleteResult} (both passed through after validation). |
| 596 | + * |
| 597 | + * @param string $promptName The prompt the argument belongs to |
| 598 | + * @param string $argumentName The argument to complete |
| 599 | + * @param callable $provider The suggestion provider |
| 600 | + * @return self For method chaining |
| 601 | + */ |
| 602 | + public function completionForPrompt( |
| 603 | + string $promptName, |
| 604 | + string $argumentName, |
| 605 | + callable $provider |
| 606 | + ): self { |
| 607 | + $this->promptCompletionProviders[$this->completionKey($promptName, $argumentName)] = $provider; |
| 608 | + $this->ensureCompletionHandler(); |
| 609 | + return $this; |
| 610 | + } |
| 611 | + |
| 612 | + /** |
| 613 | + * Register a completion provider for a resource-template argument. |
| 614 | + * |
| 615 | + * Identical contract to {@see completionForPrompt()}, but keyed on a |
| 616 | + * registered `uriTemplate` and one of its variables. The `$uriTemplate` |
| 617 | + * must match the string passed to {@see resourceTemplate()}; a completion |
| 618 | + * request naming a template that was never registered yields a -32602 |
| 619 | + * error rather than an empty result. |
| 620 | + * |
| 621 | + * @param string $uriTemplate The registered template string |
| 622 | + * @param string $argumentName The template variable to complete |
| 623 | + * @param callable $provider The suggestion provider |
| 624 | + * @return self For method chaining |
| 625 | + */ |
| 626 | + public function completionForResourceTemplate( |
| 627 | + string $uriTemplate, |
| 628 | + string $argumentName, |
| 629 | + callable $provider |
| 630 | + ): self { |
| 631 | + $this->resourceTemplateCompletionProviders[$this->completionKey($uriTemplate, $argumentName)] = $provider; |
| 632 | + $this->ensureCompletionHandler(); |
| 633 | + return $this; |
| 634 | + } |
| 635 | + |
566 | 636 | // ----------------------------------------------------------------------- |
567 | 637 | // Configuration — [Added] |
568 | 638 | // ----------------------------------------------------------------------- |
@@ -955,6 +1025,131 @@ protected function registerDefaultHandlers(): void |
955 | 1025 | }); |
956 | 1026 | } |
957 | 1027 |
|
| 1028 | + /** |
| 1029 | + * Build the composite key for a completion provider. |
| 1030 | + * |
| 1031 | + * Uses a NUL separator so a name containing the separator cannot collide |
| 1032 | + * with a different (name, argument) pair. |
| 1033 | + */ |
| 1034 | + private function completionKey(string $refName, string $argumentName): string |
| 1035 | + { |
| 1036 | + return $refName . "\0" . $argumentName; |
| 1037 | + } |
| 1038 | + |
| 1039 | + /** |
| 1040 | + * Whether any registered resource template uses the given URI template. |
| 1041 | + */ |
| 1042 | + private function hasResourceTemplate(string $uriTemplate): bool |
| 1043 | + { |
| 1044 | + foreach ($this->resourceTemplates as $template) { |
| 1045 | + if ($template->uriTemplate === $uriTemplate) { |
| 1046 | + return true; |
| 1047 | + } |
| 1048 | + } |
| 1049 | + return false; |
| 1050 | + } |
| 1051 | + |
| 1052 | + /** |
| 1053 | + * Lazily register the completion/complete handler on first provider use. |
| 1054 | + * |
| 1055 | + * Keeping registration lazy means Server::getCapabilities() only advertises |
| 1056 | + * the `completions` capability for servers that actually register a |
| 1057 | + * provider. |
| 1058 | + */ |
| 1059 | + private function ensureCompletionHandler(): void |
| 1060 | + { |
| 1061 | + if ($this->completionHandlerRegistered) { |
| 1062 | + return; |
| 1063 | + } |
| 1064 | + $this->completionHandlerRegistered = true; |
| 1065 | + |
| 1066 | + $this->server->registerHandler('completion/complete', function ($params) { |
| 1067 | + $ref = is_object($params) ? ($params->ref ?? null) : null; |
| 1068 | + $argument = is_object($params) ? ($params->argument ?? null) : null; |
| 1069 | + $argName = is_object($argument) ? ($argument->name ?? '') : ''; |
| 1070 | + $argValue = is_object($argument) ? ($argument->value ?? '') : ''; |
| 1071 | + |
| 1072 | + // Already-resolved arguments for multi-argument completion. |
| 1073 | + $context = []; |
| 1074 | + $ctx = is_object($params) ? ($params->context ?? null) : null; |
| 1075 | + if ($ctx instanceof CompletionContext) { |
| 1076 | + $context = $ctx->arguments; |
| 1077 | + } |
| 1078 | + |
| 1079 | + // An invalid *reference* is a -32602 error, not an empty result. |
| 1080 | + if ($ref instanceof PromptReference) { |
| 1081 | + if (!isset($this->promptHandlers[$ref->name])) { |
| 1082 | + throw McpServerException::unknownPrompt($ref->name); |
| 1083 | + } |
| 1084 | + $provider = $this->promptCompletionProviders[$this->completionKey($ref->name, $argName)] ?? null; |
| 1085 | + } elseif ($ref instanceof ResourceReference) { |
| 1086 | + // ResourceReference->uri carries the registered template string. |
| 1087 | + if (!$this->hasResourceTemplate($ref->uri)) { |
| 1088 | + throw McpServerException::unknownResourceTemplate($ref->uri); |
| 1089 | + } |
| 1090 | + $provider = $this->resourceTemplateCompletionProviders[$this->completionKey($ref->uri, $argName)] ?? null; |
| 1091 | + } else { |
| 1092 | + throw McpServerException::invalidCompletionRef(); |
| 1093 | + } |
| 1094 | + |
| 1095 | + // Valid ref but no provider for this specific argument: no suggestions. |
| 1096 | + if ($provider === null) { |
| 1097 | + return new CompleteResult(completion: new CompletionObject(values: [])); |
| 1098 | + } |
| 1099 | + |
| 1100 | + return $this->normalizeCompletionResult($provider($argValue, $context)); |
| 1101 | + }); |
| 1102 | + } |
| 1103 | + |
| 1104 | + /** |
| 1105 | + * Normalize a completion provider's return value into a CompleteResult. |
| 1106 | + * |
| 1107 | + * Enforces the spec's 100-value cap on the SEND side (BaseSession does not |
| 1108 | + * validate outgoing results): |
| 1109 | + * - A `string[]` longer than 100 is truncated to the first 100, with |
| 1110 | + * `hasMore: true` and `total` set to the full count; truncation is |
| 1111 | + * logged so it is not silent. |
| 1112 | + * - A hand-built CompletionObject/CompleteResult is validated (which throws |
| 1113 | + * above 100), so an author-built oversized response fails loudly at the |
| 1114 | + * source rather than emitting a spec-violating payload. |
| 1115 | + * |
| 1116 | + * @param mixed $result The raw provider return value |
| 1117 | + * @throws McpServerException If the result type is unsupported |
| 1118 | + */ |
| 1119 | + private function normalizeCompletionResult(mixed $result): CompleteResult |
| 1120 | + { |
| 1121 | + if ($result instanceof CompleteResult) { |
| 1122 | + $result->completion->validate(); |
| 1123 | + return $result; |
| 1124 | + } |
| 1125 | + |
| 1126 | + if ($result instanceof CompletionObject) { |
| 1127 | + $result->validate(); |
| 1128 | + return new CompleteResult(completion: $result); |
| 1129 | + } |
| 1130 | + |
| 1131 | + if (is_array($result)) { |
| 1132 | + $values = array_values(array_map(static fn ($v): string => (string)$v, $result)); |
| 1133 | + $total = count($values); |
| 1134 | + |
| 1135 | + if ($total > 100) { |
| 1136 | + $this->logger->debug(sprintf( |
| 1137 | + 'Completion provider returned %d values; truncating to 100 and setting hasMore=true.', |
| 1138 | + $total |
| 1139 | + )); |
| 1140 | + return new CompleteResult(completion: new CompletionObject( |
| 1141 | + values: array_slice($values, 0, 100), |
| 1142 | + total: $total, |
| 1143 | + hasMore: true, |
| 1144 | + )); |
| 1145 | + } |
| 1146 | + |
| 1147 | + return new CompleteResult(completion: new CompletionObject(values: $values)); |
| 1148 | + } |
| 1149 | + |
| 1150 | + throw McpServerException::invalidCompletionResult($result); |
| 1151 | + } |
| 1152 | + |
958 | 1153 | // ----------------------------------------------------------------------- |
959 | 1154 | // Internal — Reflection Helpers |
960 | 1155 | // ----------------------------------------------------------------------- |
|
0 commit comments