Skip to content

Commit ffb107a

Browse files
committed
Update MCP protocol to 2025 revision
1 parent 5ef9bf1 commit ffb107a

File tree

17 files changed

+404
-27
lines changed

17 files changed

+404
-27
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,11 @@ use OPGG\LaravelMcpServer\Enums\ProcessMessageType;
173173
interface ToolInterface
174174
{
175175
public function messageType(): ProcessMessageType; // New method
176+
public function title(): ?string; // Added in MCP 2025-06-18
176177
public function name(): string; // Renamed
177178
public function description(): string; // Renamed
178179
public function inputSchema(): array; // Renamed
180+
public function outputSchema(): array; // Added in MCP 2025-06-18
179181
public function annotations(): array; // Renamed
180182
public function execute(array $arguments): mixed; // No change
181183
}
@@ -219,9 +221,11 @@ class MyNewTool implements ToolInterface
219221
return false; // Most tools should return false
220222
}
221223

224+
public function title(): ?string { return 'MyNewTool'; }
222225
public function name(): string { return 'MyNewTool'; }
223226
public function description(): string { return 'This is my new tool.'; }
224227
public function inputSchema(): array { return []; }
228+
public function outputSchema(): array { return []; }
225229
public function annotations(): array { return []; }
226230
public function execute(array $arguments): mixed { /* ... */ }
227231
}
@@ -255,6 +259,8 @@ Key benefits:
255259

256260
- Real-time communication support through Streamable HTTP with SSE integration
257261
- Implementation of tools and resources compliant with Model Context Protocol specifications
262+
- Support for the 2025-06-18 MCP revision, including paginated `tools/list`,
263+
structured tool results, and capability metadata for tool list change notifications
258264
- Adapter-based design architecture with Pub/Sub messaging pattern (starting with Redis, more adapters planned)
259265
- Simple routing and middleware configuration
260266

@@ -857,6 +863,9 @@ interface ToolInterface
857863
// NEW in v1.3.0: Determines if this tool requires streaming (SSE) instead of standard HTTP.
858864
public function isStreaming(): bool;
859865

866+
// Optional user-facing title introduced in MCP 2025-06-18.
867+
public function title(): ?string;
868+
860869
// The unique, callable name of your tool (e.g., 'get-user-details').
861870
public function name(): string;
862871

@@ -866,6 +875,9 @@ interface ToolInterface
866875
// Defines the expected input parameters for your tool using a JSON Schema-like structure.
867876
public function inputSchema(): array;
868877

878+
// Optional JSON Schema describing structured output for MCP 2025-06-18 clients.
879+
public function outputSchema(): array;
880+
869881
// Provides a way to add arbitrary metadata or annotations to your tool.
870882
public function annotations(): array;
871883

@@ -899,6 +911,20 @@ Most tools should return `false` unless you specifically need real-time streamin
899911
- Live data feeds or monitoring tools
900912
- Interactive tools requiring bidirectional communication
901913

914+
**`title(): ?string` (New in MCP 2025-06-18)**
915+
916+
Return a short, human-friendly label for your tool. Clients display this label
917+
to users while still relying on `name()` for invocation. Returning `null`
918+
defaults to the `name()` value. This method was added to align with the
919+
"Tool" data type definition in the 2025-06-18 specification.
920+
921+
**`outputSchema(): array` (New in MCP 2025-06-18)**
922+
923+
Provide a JSON Schema describing your structured responses. When populated,
924+
the server will emit the schema alongside each tool definition and return
925+
`structuredContent` blocks from executions, helping clients validate outputs.
926+
Return an empty array if your tool only emits unstructured text.
927+
902928
**`name(): string`**
903929

904930
This is the identifier for your tool. It should be unique. Clients will use this name to request your tool. For example: `get-weather`, `calculate-sum`.

scripts/test-setup.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ curl -X POST "$HTTP_ENDPOINT" \
164164
"id": 1,
165165
"method": "initialize",
166166
"params": {
167-
"protocolVersion": "2024-11-05",
167+
"protocolVersion": "2025-06-18",
168168
"capabilities": {
169169
"tools": {},
170170
"resources": {}

src/Console/Commands/MakeMcpToolCommand.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ protected function buildDynamicClass(string $className): string
221221
// Extract parameters
222222
$toolName = $params['toolName'] ?? Str::kebab(preg_replace('/Tool$/', '', $className));
223223
$description = $params['description'] ?? 'Auto-generated MCP tool';
224+
$title = $params['title'] ?? Str::headline(str_replace('-', ' ', $toolName));
224225
$inputSchema = $params['inputSchema'] ?? [];
226+
$outputSchema = $params['outputSchema'] ?? [];
225227
$annotations = $params['annotations'] ?? [];
226228
$executeLogic = $params['executeLogic'] ?? ' return ["result" => "success"];';
227229
$imports = $params['imports'] ?? [];
@@ -235,6 +237,9 @@ protected function buildDynamicClass(string $className): string
235237
// Build input schema
236238
$inputSchemaString = $this->arrayToPhpString($inputSchema, 2);
237239

240+
// Build output schema
241+
$outputSchemaString = $this->arrayToPhpString($outputSchema, 2);
242+
238243
// Build annotations
239244
$annotationsString = $this->arrayToPhpString($annotations, 2);
240245

@@ -251,7 +256,9 @@ protected function buildDynamicClass(string $className): string
251256
'{{ className }}' => $className,
252257
'{{ toolName }}' => $toolName,
253258
'{{ description }}' => addslashes($description),
259+
'{{ title }}' => addslashes($title),
254260
'{{ inputSchema }}' => $inputSchemaString,
261+
'{{ outputSchema }}' => $outputSchemaString,
255262
'{{ annotations }}' => $annotationsString,
256263
'{{ executeLogic }}' => $executeLogic,
257264
'{{ imports }}' => $importsString,
@@ -317,9 +324,11 @@ protected function replaceStubPlaceholders(string $stub, string $className, stri
317324
$namespace .= '\\'.$tagDirectory;
318325
}
319326

327+
$toolTitle = Str::headline(str_replace('-', ' ', $toolName));
328+
320329
return str_replace(
321-
['{{ className }}', '{{ namespace }}', '{{ toolName }}'],
322-
[$className, $namespace, $toolName],
330+
['{{ className }}', '{{ namespace }}', '{{ toolName }}', '{{ toolTitle }}'],
331+
[$className, $namespace, $toolName, addslashes($toolTitle)],
323332
$stub
324333
);
325334
}

src/Data/Requests/NotificationData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* JSON-RPC Request Notification Data
77
* Represents the data structure for a JSON-RPC notification according to the MCP specification.
88
*
9-
* @see https://modelcontextprotocol.io/specification/2024-11-05/basic/index#notifications
9+
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#notifications
1010
*/
1111
class NotificationData
1212
{

src/Data/Requests/RequestData.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* Represents a JSON-RPC Request object according to the MCP specification.
77
* This class encapsulates the data structure for incoming requests.
88
*
9-
* @link https://modelcontextprotocol.io/specification/2024-11-05/basic/index#requests
9+
* @link https://modelcontextprotocol.io/specification/2025-06-18/basic/index#requests
1010
*
1111
* @property string $method The name of the method to be invoked.
1212
* @property string $jsonRpc The JSON-RPC version string (e.g., "2.0").

src/Data/Resources/InitializeResource.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class InitializeResource
3838
* @param array $capabilities The capabilities supported by the server.
3939
* @param string $protocolVersion The protocol version being used.
4040
*/
41-
public function __construct(string $name, string $version, array $capabilities, string $protocolVersion = '2024-11-05')
41+
public function __construct(string $name, string $version, array $capabilities, string $protocolVersion = MCPProtocol::PROTOCOL_VERSION)
4242
{
4343
$this->serverInfo = [
4444
'name' => $name,

src/Protocol/MCPProtocol.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,17 @@
2424
*/
2525
final class MCPProtocol
2626
{
27-
// This was supposed to be 2025-03-26, but I set this to 2024-11-05 because Vercel ai-sdk doesn't support it
28-
public const PROTOCOL_VERSION = '2024-11-05';
27+
/**
28+
* Protocol version advertised by the server.
29+
*
30+
* The revision date reflects the upstream MCP specification that introduced
31+
* structured tool results and paginated tool discovery.
32+
* Keeping this in sync with the spec ensures clients can safely enable the
33+
* newly required behaviour introduced in the 2025-06-18 revision.
34+
*
35+
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index
36+
*/
37+
public const PROTOCOL_VERSION = '2025-06-18';
2938

3039
private TransportInterface $transport;
3140

src/Server/Request/ToolsCallHandler.php

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace OPGG\LaravelMcpServer\Server\Request;
44

5+
use Illuminate\Contracts\Support\Arrayable;
6+
use JsonSerializable;
57
use OPGG\LaravelMcpServer\Enums\ProcessMessageType;
68
use OPGG\LaravelMcpServer\Exceptions\Enums\JsonRpcErrorCode;
79
use OPGG\LaravelMcpServer\Exceptions\JsonRpcErrorException;
@@ -64,18 +66,111 @@ public function execute(string $method, ?array $params = null): array
6466
$result = $tool->execute($arguments);
6567

6668
if ($method === 'tools/call') {
69+
$structuredContent = $this->normalizeStructuredContent($result);
70+
$content = [];
71+
72+
if ($structuredContent !== null) {
73+
$serialized = json_encode($structuredContent, JSON_UNESCAPED_UNICODE);
74+
if ($serialized === false) {
75+
throw new JsonRpcErrorException(
76+
message: 'Failed to encode structured tool response',
77+
code: JsonRpcErrorCode::INTERNAL_ERROR
78+
);
79+
}
80+
81+
$content[] = [
82+
'type' => 'text',
83+
'text' => $serialized,
84+
];
85+
86+
/**
87+
* MCP 2025-06-18 expects servers to populate both `content` and
88+
* `structuredContent` when a tool produces JSON data. This keeps
89+
* older clients functional while enabling schema validation.
90+
*
91+
* @see https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool-result
92+
*/
93+
return [
94+
'content' => $content,
95+
'structuredContent' => $structuredContent,
96+
'isError' => false,
97+
];
98+
}
99+
100+
$content[] = [
101+
'type' => 'text',
102+
'text' => $this->stringifyResult($result),
103+
];
104+
67105
return [
68-
'content' => [
69-
[
70-
'type' => 'text',
71-
'text' => is_string($result) ? $result : json_encode($result, JSON_UNESCAPED_UNICODE),
72-
],
73-
],
106+
'content' => $content,
107+
'isError' => false,
74108
];
75109
} else {
76110
return [
77111
'result' => $result,
78112
];
79113
}
80114
}
115+
116+
private function normalizeStructuredContent(mixed $result): array|null
117+
{
118+
if ($result instanceof Arrayable) {
119+
return $result->toArray();
120+
}
121+
122+
if ($result instanceof JsonSerializable) {
123+
$serialized = $result->jsonSerialize();
124+
125+
return is_array($serialized)
126+
? $serialized
127+
: (is_object($serialized) ? (array) $serialized : null);
128+
}
129+
130+
if (is_array($result)) {
131+
return $result;
132+
}
133+
134+
if (is_object($result)) {
135+
$encoded = json_encode($result, JSON_UNESCAPED_UNICODE);
136+
if ($encoded === false) {
137+
return null;
138+
}
139+
140+
$decoded = json_decode($encoded, true);
141+
142+
return is_array($decoded) ? $decoded : null;
143+
}
144+
145+
return null;
146+
}
147+
148+
private function stringifyResult(mixed $result): string
149+
{
150+
if (is_string($result)) {
151+
return $result;
152+
}
153+
154+
if (is_scalar($result) || $result === null) {
155+
$encoded = json_encode($result, JSON_UNESCAPED_UNICODE);
156+
if ($encoded === false) {
157+
throw new JsonRpcErrorException(
158+
message: 'Failed to encode scalar tool response',
159+
code: JsonRpcErrorCode::INTERNAL_ERROR
160+
);
161+
}
162+
163+
return $encoded;
164+
}
165+
166+
$encoded = json_encode($result, JSON_UNESCAPED_UNICODE);
167+
if ($encoded === false) {
168+
throw new JsonRpcErrorException(
169+
message: 'Failed to encode complex tool response',
170+
code: JsonRpcErrorCode::INTERNAL_ERROR
171+
);
172+
}
173+
174+
return $encoded;
175+
}
81176
}

src/Server/Request/ToolsListHandler.php

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace OPGG\LaravelMcpServer\Server\Request;
44

55
use OPGG\LaravelMcpServer\Enums\ProcessMessageType;
6+
use OPGG\LaravelMcpServer\Exceptions\Enums\JsonRpcErrorCode;
7+
use OPGG\LaravelMcpServer\Exceptions\JsonRpcErrorException;
68
use OPGG\LaravelMcpServer\Protocol\Handlers\RequestHandler;
79
use OPGG\LaravelMcpServer\Services\ToolService\ToolRepository;
810

@@ -12,6 +14,8 @@ class ToolsListHandler extends RequestHandler
1214

1315
protected const HANDLE_METHOD = 'tools/list';
1416

17+
private const PAGE_SIZE = 50;
18+
1519
private ToolRepository $toolRepository;
1620

1721
public function __construct(ToolRepository $toolRepository)
@@ -22,8 +26,37 @@ public function __construct(ToolRepository $toolRepository)
2226

2327
public function execute(string $method, ?array $params = null): array
2428
{
29+
$cursor = $params['cursor'] ?? null;
30+
$offset = 0;
31+
32+
if ($cursor !== null) {
33+
if (is_string($cursor) && ctype_digit($cursor)) {
34+
$offset = (int) $cursor;
35+
} elseif (is_int($cursor) && $cursor >= 0) {
36+
$offset = $cursor;
37+
} else {
38+
throw new JsonRpcErrorException(
39+
message: 'Invalid cursor provided for tools/list pagination',
40+
code: JsonRpcErrorCode::INVALID_PARAMS
41+
);
42+
}
43+
}
44+
45+
$schemas = $this->toolRepository->getToolSchemas();
46+
$paginated = array_slice($schemas, $offset, self::PAGE_SIZE);
47+
$hasMore = ($offset + self::PAGE_SIZE) < count($schemas);
48+
49+
/**
50+
* Pagination behaviour follows MCP spec 2025-06-18 tools/list guidance, which
51+
* requires returning a nextCursor token when more data is available. The
52+
* cursor we emit is a simple numeric offset because the spec allows server
53+
* defined cursors. See "Listing Tools" section of the protocol revision.
54+
*
55+
* @see https://modelcontextprotocol.io/specification/2025-06-18/server/tools#listing-tools
56+
*/
2557
return [
26-
'tools' => $this->toolRepository->getToolSchemas(),
58+
'tools' => $paginated,
59+
'nextCursor' => $hasMore ? (string) ($offset + self::PAGE_SIZE) : null,
2760
];
2861
}
2962
}

src/Server/ServerCapabilities.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ final class ServerCapabilities
6161
public function withTools(?array $config = []): self
6262
{
6363
$this->supportsTools = true;
64+
$config = $config ?? [];
65+
66+
if (! array_key_exists('listChanged', $config)) {
67+
/**
68+
* MCP 2025-06-18 mandates advertising whether the server emits
69+
* tools/list_changed notifications. Default to false to reflect the
70+
* package's current behaviour while remaining standards compliant.
71+
*
72+
* @see https://modelcontextprotocol.io/specification/2025-06-18/server/tools#capabilities
73+
*/
74+
$config['listChanged'] = false;
75+
}
76+
6477
$this->toolsConfig = $config;
6578

6679
return $this;

0 commit comments

Comments
 (0)