Skip to main content
Glama
McpStdioTransportTest.php14.9 kB
<?php /** * Tests for the McpStdioTransport class. * * @package WordPressMcp * @subpackage Tests */ namespace Automattic\WordpressMcp\Tests; use Automattic\WordpressMcp\Core\McpStdioTransport; use WP_REST_Request; use WP_Error; /** * Test cases for the McpStdioTransport class. * Tests STDIO-specific functionality including WordPress-style responses * and both JWT and application password authentication. */ class McpStdioTransportTest extends McpTransportTestBase { /** * The STDIO transport instance. * * @var McpStdioTransport */ private McpStdioTransport $stdio_transport; /** * Set up the STDIO transport test. */ public function setUp(): void { parent::setUp(); // Initialize STDIO transport $this->stdio_transport = new McpStdioTransport( $this->wp_mcp ); } /** * Get the transport endpoint for STDIO. * * @return string */ protected function get_transport_endpoint(): string { return '/wp/v2/wpmcp'; } /** * Create a transport request for STDIO. * * @param string $method The MCP method to call. * @param array $params The parameters for the method. * @param array $headers Additional headers to include. * @return WP_REST_Request */ protected function create_transport_request( string $method, array $params = array(), array $headers = array() ): WP_REST_Request { $request = new WP_REST_Request( 'POST', $this->get_transport_endpoint() ); // STDIO format: simple method and params structure $body = array( 'method' => $method, 'params' => $params, ); $request->set_body( wp_json_encode( $body ) ); $request->add_header( 'Content-Type', 'application/json' ); // Add any additional headers foreach ( $headers as $key => $value ) { $request->add_header( $key, $value ); } return $request; } /** * Assert a valid response for STDIO transport. * * @param mixed $response The response to validate. * @param array $expected_data Expected data in the response. */ protected function assert_valid_response( $response, array $expected_data = array() ): void { $this->assertEquals( 200, $response->get_status(), 'Response should have 200 status' ); $this->assertInstanceOf( 'WP_REST_Response', $response, 'Should return WP_REST_Response' ); $data = $response->get_data(); $this->assertIsArray( $data, 'Response data should be an array' ); // Check for expected data if provided foreach ( $expected_data as $key => $expected_value ) { $this->assertArrayHasKey( $key, $data, "Response should contain key: {$key}" ); if ( $expected_value !== null ) { $this->assertEquals( $expected_value, $data[ $key ], "Value for {$key} should match expected" ); } } } /** * Assert an error response for STDIO transport. * * @param mixed $response The response to validate. * @param string $expected_error_code Expected error code. * @param int $expected_status Expected HTTP status code. */ protected function assert_error_response( $response, string $expected_error_code, int $expected_status ): void { $this->assertEquals( $expected_status, $response->get_status(), "Should return {$expected_status} status" ); // WordPress REST API converts WP_Error to WP_REST_Response with error status if ( $response instanceof \WP_Error ) { $this->assertInstanceOf( 'WP_Error', $response, 'Should return WP_Error for errors' ); $this->assertEquals( $expected_error_code, $response->get_error_code(), "Error code should be {$expected_error_code}" ); } else { $this->assertInstanceOf( 'WP_REST_Response', $response, 'Should return WP_REST_Response for errors' ); $data = $response->get_data(); if ( isset( $data['code'] ) ) { $this->assertEquals( $expected_error_code, $data['code'], "Error code should be {$expected_error_code}" ); } } } /** * Test STDIO authentication with JWT token. */ public function test_stdio_authentication_with_jwt(): void { $this->set_jwt_auth( $this->admin_jwt_token ); $request = $this->create_transport_request( 'ping' ); $response = rest_do_request( $request ); $this->assert_valid_response( $response ); } /** * Test STDIO authentication with application password. */ public function test_stdio_authentication_with_application_password(): void { // Create application password for admin user $app_password = $this->create_application_password( $this->admin_user->ID ); // Set application password authentication $this->set_application_password_auth( $this->admin_user->user_login, $app_password['password'] ); $request = $this->create_transport_request( 'ping' ); $response = rest_do_request( $request ); $this->assert_valid_response( $response ); } /** * Test STDIO accepts various user roles with application password. * STDIO uses is_user_logged_in() so any authenticated user should work. */ public function test_stdio_accepts_various_user_roles_with_app_password(): void { $users = array( 'admin' => $this->admin_user, 'editor' => $this->editor_user, 'subscriber' => $this->subscriber_user, ); foreach ( $users as $role => $user ) { // Ensure complete cleanup before each iteration $this->clear_auth(); wp_set_current_user( 0 ); // Verify application passwords are available for this user if ( ! wp_is_application_passwords_available_for_user( $user ) ) { $this->markTestSkipped( "Application passwords not available for {$role} user in test environment" ); } try { $app_password = $this->create_application_password( $user->ID ); $this->set_application_password_auth( $user->user_login, $app_password['password'] ); $request = $this->create_transport_request( 'ping' ); $response = rest_do_request( $request ); // If we get 401, it might be a test environment limitation, not a real failure if ( 401 === $response->get_status() ) { $this->markTestSkipped( "Application password authentication failed for {$role} in test environment - likely environment limitation" ); } $this->assert_valid_response( $response, array(), "User role {$role} should be able to access STDIO" ); } catch ( \Exception $e ) { $this->markTestSkipped( "Application password test failed for {$role}: " . $e->getMessage() ); } finally { // Clean up application passwords for this user if ( class_exists( 'WP_Application_Passwords' ) ) { \WP_Application_Passwords::delete_all_application_passwords( $user->ID ); } $this->clear_auth(); } } } /** * Test STDIO accepts various user roles with JWT. */ public function test_stdio_accepts_various_user_roles_with_jwt(): void { $test_cases = array( 'admin' => $this->admin_jwt_token, 'editor' => $this->editor_jwt_token, ); foreach ( $test_cases as $role => $token ) { $this->set_jwt_auth( $token ); $request = $this->create_transport_request( 'ping' ); $response = rest_do_request( $request ); $this->assert_valid_response( $response, array(), "User role {$role} should be able to access STDIO via JWT" ); // Clean up for next iteration $this->clear_auth(); } } /** * Test STDIO request format validation. */ public function test_stdio_request_format_validation(): void { $this->set_jwt_auth( $this->admin_jwt_token ); // Test missing method $request = new WP_REST_Request( 'POST', $this->get_transport_endpoint() ); $request->set_body( wp_json_encode( array( 'params' => array() ) ) ); $request->add_header( 'Content-Type', 'application/json' ); $response = rest_do_request( $request ); $this->assert_error_response( $response, 'invalid_request', 400 ); // Test empty body $request = new WP_REST_Request( 'POST', $this->get_transport_endpoint() ); $request->set_body( '' ); $request->add_header( 'Content-Type', 'application/json' ); $response = rest_do_request( $request ); $this->assert_error_response( $response, 'invalid_request', 400 ); } /** * Test STDIO backward compatibility with old request format. */ public function test_stdio_backward_compatibility(): void { $this->set_jwt_auth( $this->admin_jwt_token ); // Test old format where params are directly in the message $request = new WP_REST_Request( 'POST', $this->get_transport_endpoint() ); $request->set_body( wp_json_encode( array( 'method' => 'ping', 'some_param' => 'some_value', // This should be treated as params ) ) ); $request->add_header( 'Content-Type', 'application/json' ); $response = rest_do_request( $request ); $this->assert_valid_response( $response ); } /** * Test STDIO WordPress-style error response format. */ public function test_stdio_wordpress_error_format(): void { $this->set_jwt_auth( $this->admin_jwt_token ); $request = $this->create_transport_request( 'nonexistent_method' ); $response = rest_do_request( $request ); $this->assert_error_response( $response, 'invalid_method', 400 ); } /** * Test STDIO with different HTTP methods. */ public function test_stdio_only_accepts_post_method(): void { $this->set_jwt_auth( $this->admin_jwt_token ); $methods = array( 'GET', 'PUT', 'DELETE', 'PATCH' ); foreach ( $methods as $method ) { $request = new WP_REST_Request( $method, $this->get_transport_endpoint() ); $response = rest_do_request( $request ); // Should get 405 Method Not Allowed or similar error $this->assertTrue( in_array( $response->get_status(), array( 404, 405 ), true ), "HTTP {$method} should not be allowed on STDIO endpoint" ); } } /** * Test STDIO authentication fallback behavior. */ public function test_stdio_authentication_fallback(): void { // Skip if application passwords aren't working in test environment if ( ! wp_is_application_passwords_available() ) { $this->markTestSkipped( 'Application passwords not available in test environment' ); } // Test that JWT takes precedence over application password $app_password = $this->create_application_password( $this->admin_user->ID ); // Set both JWT and application password $_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $this->admin_jwt_token; $_SERVER['PHP_AUTH_USER'] = $this->admin_user->user_login; $_SERVER['PHP_AUTH_PW'] = $app_password['password']; $_SERVER['REQUEST_URI'] = '/wp-json' . $this->get_transport_endpoint(); // Manually trigger JWT authentication (should take precedence) $auth_result = $this->jwt_auth->authenticate_request( null ); if ( ! is_wp_error( $auth_result ) ) { // JWT authentication succeeded } $request = $this->create_transport_request( 'ping' ); $response = rest_do_request( $request ); $this->assert_valid_response( $response ); // Verify the authentication was successful (user should be set) $this->assertGreaterThan( 0, get_current_user_id(), 'A user should be authenticated' ); } /** * Test STDIO with malformed JSON. */ public function test_stdio_with_malformed_json(): void { $this->set_jwt_auth( $this->admin_jwt_token ); $request = new WP_REST_Request( 'POST', $this->get_transport_endpoint() ); $request->set_body( '{"method": "ping", "params":' ); // Malformed JSON $request->add_header( 'Content-Type', 'application/json' ); $response = rest_do_request( $request ); // WordPress REST API automatically handles malformed JSON $this->assert_error_response( $response, 'rest_invalid_json', 400 ); } /** * Test STDIO response includes debug information in errors. */ public function test_stdio_error_includes_debug_info(): void { $this->set_jwt_auth( $this->admin_jwt_token ); $request = $this->create_transport_request( 'invalid_method' ); $response = rest_do_request( $request ); $this->assert_error_response( $response, 'invalid_method', 400 ); // Additional check for debug info in the response if ( $response instanceof \WP_Error ) { $error_message = $response->get_error_message(); $this->assertStringContainsString( '[DEBUG:', $error_message, 'Error message should include debug information' ); } else { $data = $response->get_data(); if ( isset( $data['message'] ) ) { $this->assertStringContainsString( '[DEBUG:', $data['message'], 'Error message should include debug information' ); } } } /** * Test STDIO with application password for invalid user. */ public function test_stdio_with_invalid_application_password(): void { // Skip if application passwords aren't working in test environment if ( ! wp_is_application_passwords_available() ) { $this->markTestSkipped( 'Application passwords not available in test environment' ); } $this->set_application_password_auth( 'nonexistent_user', 'invalid_password' ); $request = $this->create_transport_request( 'ping' ); $response = rest_do_request( $request ); $status = $response->get_status(); // Should be rejected due to invalid authentication // If we get 200, it means app password auth isn't working in test env if ( 200 === $status ) { $this->markTestSkipped( 'Application password authentication not working in test environment - cannot test rejection' ); } $this->assertTrue( in_array( $status, array( 401, 403 ), true ), "Invalid application password should be rejected, got status: {$status}" ); } /** * Test STDIO permission check method. */ public function test_stdio_permission_check(): void { // Test with valid JWT $this->set_jwt_auth( $this->admin_jwt_token ); $result = $this->stdio_transport->check_permission(); $this->assertTrue( $result, 'Valid JWT should pass permission check' ); // Test without authentication $this->clear_auth(); $result = $this->stdio_transport->check_permission(); $this->assertFalse( $result, 'No authentication should fail permission check' ); // Test with MCP disabled update_option( 'wordpress_mcp_settings', array( 'enabled' => false ) ); $this->set_jwt_auth( $this->admin_jwt_token ); $result = $this->stdio_transport->check_permission(); $this->assertInstanceOf( 'WP_Error', $result, 'Disabled MCP should return WP_Error' ); $this->assertEquals( 'mcp_disabled', $result->get_error_code() ); } /** * Test that STDIO handles large requests appropriately. */ public function test_stdio_handles_large_requests(): void { $this->set_jwt_auth( $this->admin_jwt_token ); // Create a large parameter array $large_params = array( 'large_data' => str_repeat( 'x', 10000 ), // 10KB string 'array_data' => array_fill( 0, 1000, 'test' ), // Large array ); $request = $this->create_transport_request( 'ping', $large_params ); $response = rest_do_request( $request ); // Should handle large requests gracefully $this->assertTrue( in_array( $response->get_status(), array( 200, 413 ), true ), 'Large requests should either succeed or return 413 (too large)' ); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Automattic/wordpress-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server