Skip to content

Closes #1220: support media lookup by filename or URL in MCP media abilities - #1225

Open
Miraeld wants to merge 2 commits into
developfrom
fix/1220-mcp-media-lookup
Open

Closes #1220: support media lookup by filename or URL in MCP media abilities#1225
Miraeld wants to merge 2 commits into
developfrom
fix/1220-mcp-media-lookup

Conversation

@Miraeld

@Miraeld Miraeld commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #1220

The MCP media actions only accepted a numeric attachment ID, so Optimize hero-banner.jpg was not
possible: the user had to open the Media Library, edit the image and copy the ID out of the browser
URL first. imagify/optimize-media, imagify/restore-media and imagify/get-media-status now also
accept media_filename and media_url, so an assistant can act on the identifier the user actually
has.

Type of change

  • New feature (non-breaking change which adds functionality).

Detailed scenario

What was tested

Automated — integration (--group MediaResolver, 20 tests / 48 assertions), against a real
database and real WP_Query:
resolution by ID, by file name, case-insensitively, by relative path
(2026/08/hero.jpg), by URL, and by derivative URLs (hero.jpg.webp, hero.jpg.avif,
hero-300x200.jpg, hero-300x200.jpg.webp). Plus the failure paths: unknown file name, unknown URL,
ambiguous file name (asserting the candidate IDs appear in the message), and every unusable-input
shape ([], media_id: 0, media_id: -5, blank URL, blank file name). One test specifically pins
that hero.jpg does not resolve to my-hero.jpg or hero.jpg.bak.

Automated — integration, per ability: all three abilities register media_filename and
media_url and no longer mark any input as required. imagify/get-media-status executed with
media_filename returns output identical to the same media by media_id. imagify/restore-media
reaches its restore logic on a resolvable file name and reports the unknown one explicitly.

Automated — unit: 490 unit tests pass, including the existing media-ability tests updated for the
new error message.

Manual, on a real site (WP 7.0.2, Local, populated Media Library): for three real attachments,
media_filename, media_url and a next-gen …png.webp URL each resolved to the correct attachment
ID, and imagify/get-media-status returned byte-identical output whether called with
media_filename or media_id.

#118 pr1203-transient2-scaled.png
  filename -> 118    url -> 118    url.webp -> 118
imagify_media_not_found: No media in the library is named "definitely-not-here.jpg".
imagify_missing_media_identifier: Provide one of media_id, media_url, or media_filename to identify the media.

composer phpcs and composer run-stan are clean.

How to test

  1. On a site with WP 6.9+ and the MCP server reachable, note a file name in the Media Library, for
    example hero-banner.jpg.
  2. Call imagify/get-media-status with {"media_filename": "hero-banner.jpg"} and then with
    {"media_id": <that attachment's ID>} — the two responses must be identical.
  3. Call it with {"media_url": "<the attachment URL>"}, then again with .webp appended to that URL
    — both must resolve to the same media.
  4. Upload the same file name into two different months, then call with that file name: the error must
    name both attachment IDs.
  5. Call with {}: the error must list the three accepted inputs.
  6. Repeat step 2 against imagify/optimize-media and imagify/restore-media.

Affected Features & Quality Assurance Scope

The three MCP media abilities. media_id behaviour is unchanged, including imagify/restore-media's
existing fallback to the custom-folders context. No non-MCP code path is touched: the Media Library
UI, bulk optimization and the upload flow are untouched.

Technical description

Documentation

Imagify\Abilities\MediaResolver::resolve_id( array $args ) returns an attachment ID or a WP_Error.
Precedence is media_id, then media_url, then media_filename; a media_id of 0 or less counts
as absent, so the next identifier is tried. Each ability calls it first and converts a WP_Error into
its own status: "error" output shape, so no output schema changed.

media_url goes through attachment_url_to_postid(). Because a front-end URL often points at a
derivative rather than the original, a miss retries as a file-name lookup after peeling off the
next-gen extension and the -300x200 dimension suffix, so hero-300x200.jpg.webp still finds
hero.jpg.

media_filename narrows candidates with a LIKE on _wp_attached_file, then keeps only the rows
whose own base name matches exactly and case-insensitively. That second pass is the important part: a
bare LIKE '%hero.jpg%' also matches my-hero.jpg and hero.jpg.bak, which would mean optimizing or
restoring the wrong file. A full relative path is accepted and reduced to its base name.

Ambiguity is never resolved silently. When several attachments share a name the error lists their IDs
so the caller can retry with media_id.

The two new schema properties come from MediaResolver::get_input_schema_properties() so the three
abilities cannot drift apart. docs/api/mcp.md gains an "Identifying a media" section documenting
precedence, the derivative-URL fallback and the three error codes.

New dependencies

None. Uses attachment_url_to_postid() and WP_Query.

Risks

The file-name lookup adds one WP_Query with a postmeta LIKE, which cannot use an index on the
value. It is bounded: posts_per_page is capped at 20 via MAX_CANDIDATES, no_found_rows skips the
count query, and it only runs on an explicit one-shot MCP call — never in a page render or a bulk
loop. It is also skipped entirely whenever media_id is supplied, so existing callers pay nothing.

Dropping media_id from required means a caller can now send no identifier at all. That is the
point of the change, and the resolver answers with an explicit message naming the three accepted
inputs rather than falling through to a vague failure.

The derivative-URL fallback is a deliberate best-effort guess and only runs after
attachment_url_to_postid() has already failed, so it cannot change the result for a URL that
resolves normally. If the guess is itself ambiguous, the ambiguity error applies as usual.

Mandatory Checklist

Code validation

  • I validated all the Acceptance Criteria. If possible, provide screenshots or videos.
  • I triggered all changed lines of code at least once without new errors/warnings/notices.
  • I implemented built-in tests to cover the new/changed code.

Code style

  • I wrote a self-explanatory code about what it does.
  • I protected entry points against unexpected inputs.
  • I did not introduce unnecessary complexity.
  • Output messages (errors, notices, logs) are explicit enough for users to understand the issue and are actionnable.

Unticked items justification

All mandatory items apply and are done.

Additional Checks

  • In the case of complex code, I wrote comments to explain it.
  • When possible, I prepared ways to observe the implemented system (logs, data, etc.)
  • I added error handling logic when using functions that could throw errors (HTTP/API request, filesystem, etc.)

The optimize-media, restore-media and get-media-status abilities only
accepted a numeric attachment ID, so a caller had to open the Media
Library and copy an ID out of the URL before asking for anything.

Add two interchangeable inputs, media_filename and media_url, resolved
by MediaResolver::resolve_id(). Filenames are matched on the exact base
name of _wp_attached_file, so hero.jpg no longer also matches
my-hero.jpg. Next-gen and thumbnail URLs peel their suffixes off and
resolve to the original attachment. An ambiguous filename returns the
matching IDs instead of acting on the first match.
@codacy-production

codacy-production Bot commented Aug 10, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 25 complexity · 0 duplication

Metric Results
Complexity 25
Duplication 0

View in Codacy

🔴 Coverage 11.48% diff coverage · +0.01% coverage variation

Metric Results
Coverage variation +0.01% coverage variation (-0.10%)
Diff coverage 11.48% diff coverage (50.00%)

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (6c28250) 20312 1540 7.58%
Head commit (93740a6) 20396 (+84) 1548 (+8) 7.59% (+0.01%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#1225) 122 14 11.48%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Codacy ingests coverage from the unit suite only, so the integration
tests for the resolver are invisible to it. Unit-cover the paths that do
not need a database: identifier precedence, the URL lookup, and every
unusable-input shape.
@Miraeld

Miraeld commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Note on the one red check — Codacy Diff Coverage.

composer code-coverage runs the unit suite only (--testsuite unit), and that clover report is
the only thing uploaded to Codacy. The filename-resolution half of MediaResolver runs a real
WP_Query against the _wp_attached_file meta, so it is covered by integration tests
(--group MediaResolver, 20 tests / 48 assertions) that Codacy structurally never sees. The paths
that do not need a database — identifier precedence, the URL lookup, every unusable-input shape — are
unit-covered in Tests/Unit/classes/Abilities/MediaResolver/resolveId.php.

I deliberately did not add a query-factory injection point just to make the DB-bound branch
unit-testable: a test-only seam on a resolver whose entire job is a meta query would make the
production code worse, and AGENTS.md prefers integration tests for exactly this kind of code.

So this is a gate mismatch rather than untested code. Happy to either wire the integration suite into
the coverage report or take the override — your call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support media lookup by filename for MCP media actions

1 participant