Skip to content
Open
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
51 changes: 39 additions & 12 deletions includes/Tools/McpMediaTools.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,15 +315,15 @@ public function get_media_file_permission_callback(): bool {
*/
public function wp_upload_media_pre_callback( $args ): array {
$params = array(
'args' => $args,
'args' => array(),
);
if ( ! isset( $params['args']['file'] ) ) {
return $params;
if ( ! isset( $args['file'] ) ) {
return array( 'args' => $args );
}

try {
// Get the base64 data.
$base64_data = $params['args']['file'];
$base64_data = $args['file'];

// Remove data URI prefix if present.
if ( strpos( $base64_data, 'data:' ) === 0 ) {
Expand All @@ -346,20 +346,47 @@ public function wp_upload_media_pre_callback( $args ): array {
}

// Generate a filename based on the title or use a default.
$filename = isset( $params['args']['title'] ) ? sanitize_file_name( $params['args']['title'] ) : 'upload';
$filename = isset( $args['title'] ) ? sanitize_file_name( $args['title'] ) : 'upload';
$filename .= '.' . $this->get_extension_from_mime_type( $mime_type );

// Set up the headers for the REST API.
// Create multipart boundary.
$boundary = wp_generate_password( 24, false );

// Build multipart body with file and metadata.
$body_parts = array();

// Add the file part.
$body_parts[] = '--' . $boundary;
$body_parts[] = 'Content-Disposition: form-data; name="file"; filename="' . $filename . '"';
$body_parts[] = 'Content-Type: ' . $mime_type;
$body_parts[] = '';
$body_parts[] = $file_data;

// Add metadata fields if present.
$metadata_fields = array( 'title', 'caption', 'description', 'alt_text' );
foreach ( $metadata_fields as $field ) {
if ( isset( $args[ $field ] ) && ! empty( $args[ $field ] ) ) {
$body_parts[] = '--' . $boundary;
$body_parts[] = 'Content-Disposition: form-data; name="' . $field . '"';
$body_parts[] = '';
$body_parts[] = $args[ $field ];
}
}

// Close the multipart body.
$body_parts[] = '--' . $boundary . '--';
$body_parts[] = '';

// Set up the headers for multipart request.
$params['headers'] = array(
'content_type' => array( $mime_type ),
'content_disposition' => array( 'attachment; filename="' . $filename . '"' ),
'Content-Type' => array( 'multipart/form-data; boundary=' . $boundary ),
);

// Set the raw file data.
$params['body'] = $file_data;
// Set the multipart body.
$params['body'] = implode( "\r\n", $body_parts );

// Remove the original file parameter to avoid confusion.
unset( $params['args']['file'] );
// Clear args since everything is now in the body.
$params['args'] = array();

return $params;
} catch ( \Exception $e ) {
Expand Down
94 changes: 94 additions & 0 deletions tests/phpunit/Tools/McpMediaToolsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,100 @@ public function test_wp_upload_media_tool(): void {
$this->assertStringContainsString( 'Uploaded-Test-Image', $response->get_data()['content'][0]['text'] );
}

/**
* Test the wp_upload_media tool with complete metadata.
*/
public function test_wp_upload_media_tool_with_metadata(): void {
// Create a test image file.
$test_image_path = __DIR__ . '/../../assets/test-image.jpeg';
$test_image_data = file_get_contents( $test_image_path );
$base64_data = base64_encode( $test_image_data );

// Create a REST request.
$request = new WP_REST_Request( 'POST', '/wp/v2/wpmcp' );

// Set the request body as JSON with complete metadata.
$request->set_body(
wp_json_encode(
array(
'method' => 'tools/call',
'name' => 'wp_upload_media',
'arguments' => array(
'file' => $base64_data,
'title' => 'Complete Metadata Test',
'caption' => 'Test image caption',
'description' => 'Test image description',
'alt_text' => 'Test image alt text',
),
)
)
);

// Set content type header.
$request->add_header( 'Content-Type', 'application/json' );

// Set the current user.
wp_set_current_user( $this->admin_user->ID );

// Dispatch the request.
$response = rest_do_request( $request );

// Get the uploaded attachment ID for cleanup.
$response_text_content = json_decode( $response->get_data()['content'][0]['text'], true );

// Delete image after test (avoid duplicate media).
if ( isset( $response_text_content['id'] ) ) {
wp_delete_attachment( $response_text_content['id'], true );
}

// Check the response.
$this->assertEquals( 200, $response->get_status() );
$this->assertArrayHasKey( 'content', $response->get_data() );
$this->assertIsArray( $response->get_data()['content'] );
$this->assertCount( 1, $response->get_data()['content'] );
$this->assertEquals( 'text', $response->get_data()['content'][0]['type'] );

// Verify metadata was preserved in the response.
$this->assertStringContainsString( 'Complete-Metadata-Test', $response->get_data()['content'][0]['text'] );
}

/**
* Test the wp_upload_media_pre_callback directly for multipart handling.
*/
public function test_wp_upload_media_pre_callback_multipart_format(): void {
$media_tools = new \Automattic\WordpressMcp\Tools\McpMediaTools();

// Create test data.
$test_image_data = 'fake-image-data-for-testing';
$base64_data = base64_encode( $test_image_data );

$args = array(
'file' => $base64_data,
'title' => 'Test Title',
'alt_text' => 'Test Alt Text',
'caption' => 'Test Caption',
'description' => 'Test Description',
);

// Call the pre-callback.
$result = $media_tools->wp_upload_media_pre_callback( $args );

// Verify multipart structure.
$this->assertArrayHasKey( 'headers', $result );
$this->assertArrayHasKey( 'Content-Type', $result['headers'] );
$this->assertStringContainsString( 'multipart/form-data; boundary=', $result['headers']['Content-Type'][0] );

$this->assertArrayHasKey( 'body', $result );
$this->assertStringContainsString( 'Content-Disposition: form-data; name="file"', $result['body'] );
$this->assertStringContainsString( 'Content-Disposition: form-data; name="title"', $result['body'] );
$this->assertStringContainsString( 'Content-Disposition: form-data; name="alt_text"', $result['body'] );
$this->assertStringContainsString( 'Content-Disposition: form-data; name="caption"', $result['body'] );
$this->assertStringContainsString( 'Content-Disposition: form-data; name="description"', $result['body'] );

// Verify args are cleared.
$this->assertEmpty( $result['args'] );
}

/**
* Test the wp_update_media tool.
*/
Expand Down