diff --git a/modules/mcp/controllers/ServerController.php b/modules/mcp/controllers/ServerController.php index e11e4dc..2296ed3 100644 --- a/modules/mcp/controllers/ServerController.php +++ b/modules/mcp/controllers/ServerController.php @@ -34,6 +34,25 @@ public function indexAction() return; } + // A non-POST (a browser GET, a link click) isn't a JSON-RPC call. Per the Streamable HTTP transport, + // GET is for an SSE stream we don't offer in v1 → 405, but with a human-readable "what is this + how + // to use it" body so hitting /mcp in a browser explains itself instead of a cryptic parse error. + if (strtoupper((string) $this->getRequest()->getMethod()) !== 'POST') { + $resp->setHttpResponseCode(405); + $resp->setHeader('Allow', 'POST', true); + $this->_emit([ + 'name' => 'Tiger', + 'version' => Tiger_Version::VERSION, + 'protocolVersion' => Tiger_Mcp::PROTOCOL_VERSION, + 'transport' => 'streamable-http', + 'message' => 'This is Tiger\'s MCP endpoint. POST a JSON-RPC 2.0 request (Content-Type: ' + . 'application/json) — e.g. {"jsonrpc":"2.0","id":1,"method":"initialize"}. ' + . 'Interactive GET/SSE is not supported in v1; connect an MCP client, or test ' + . 'with the MCP Inspector (npx @modelcontextprotocol/inspector) or curl.', + ]); + return; + } + $msg = json_decode($this->_rawBody(), true); if (!is_array($msg)) { $resp->setHttpResponseCode(400); diff --git a/tests/Integration/Mcp/McpControllerTest.php b/tests/Integration/Mcp/McpControllerTest.php index 42ee408..314b8bd 100644 --- a/tests/Integration/Mcp/McpControllerTest.php +++ b/tests/Integration/Mcp/McpControllerTest.php @@ -58,6 +58,17 @@ public function the_endpoint_is_404_when_disabled(): void $this->assertSame(404, $code, 'off by default'); } + #[Test] + public function a_browser_get_returns_a_helpful_405_not_a_parse_error(): void + { + $this->enableMcp(); + $res = $this->dispatchAction(FakeMcpController::class, 'index', [], 'GET'); + $this->assertSame(405, $res->getHttpResponseCode(), 'GET is not a JSON-RPC call'); + $out = json_decode($this->echoed, true); + $this->assertSame('Tiger', $out['name']); + $this->assertStringContainsString('POST a JSON-RPC', $out['message']); + } + #[Test] public function initialize_returns_serverinfo_when_enabled(): void {