Skip to content

Add 11 handler filter hooks for extensibility#151

Merged
galatanovidiu merged 10 commits intorelease/v0.5.0from
feature/handler-filter-hooks
Mar 26, 2026
Merged

Add 11 handler filter hooks for extensibility#151
galatanovidiu merged 10 commits intorelease/v0.5.0from
feature/handler-filter-hooks

Conversation

@galatanovidiu
Copy link
Copy Markdown
Contributor

@galatanovidiu galatanovidiu commented Mar 20, 2026

Summary

11 new apply_filters hooks across the MCP handler lifecycle. Covers execution gates, argument/result transformation, list operations, initialization, and component naming.

Motivation

The plugin had 20 hooks but none in the request/response lifecycle. Once a request entered RequestRouter, there were zero extension points until the JSON-RPC response left. No way to modify arguments, filter results, block execution, or dynamically adjust component lists.

Hooks added

Pre-execution (can short-circuit with WP_Error)

Hook Params Purpose
mcp_adapter_pre_tool_call (array $args, string $tool_name, McpTool $mcp_tool, McpServer $server) => array|WP_Error Modify tool arguments or return WP_Error to block execution
mcp_adapter_pre_resource_read (array $params, string $uri, McpResource $mcp_resource, McpServer $server) => array|WP_Error Modify resource params or return WP_Error to block execution
mcp_adapter_pre_prompt_get (array $arguments, string $prompt_name, McpPrompt $mcp_prompt, McpServer $server) => array|WP_Error Modify prompt arguments or return WP_Error to block execution

Result filters

Hook Params Purpose
mcp_adapter_tool_call_result (mixed $result, array $args, string $tool_name, McpTool $mcp_tool, McpServer $server) => mixed Transform tool result before DTO assembly (PII redaction, audit logging)
mcp_adapter_resource_read_result (mixed $contents, array $params, string $uri, McpResource $mcp_resource, McpServer $server) => mixed Transform resource contents before DTO conversion
mcp_adapter_prompt_get_result (mixed $result, array $arguments, string $prompt_name, McpPrompt $mcp_prompt, McpServer $server) => mixed Transform prompt result before normalization

List filters

Hook Params Purpose
mcp_adapter_tools_list (array $tools, McpServer $server) => array Hide tools per user/role, add dynamic tools, reorder
mcp_adapter_resources_list (array $resources, McpServer $server) => array Filter resources by context, add dynamic resources
mcp_adapter_prompts_list (array $prompts, McpServer $server) => array Filter prompts, add dynamic prompts

Initialize

Hook Params Purpose
mcp_adapter_initialize_response (InitializeResult $result, McpServer $server) => InitializeResult Dynamic capabilities, custom instructions

Naming

Hook Params Purpose
mcp_adapter_prompt_name (string $name, WP_Ability $ability) => string Naming parity with existing mcp_adapter_tool_name and mcp_adapter_resource_name

Why the names differ from #130

The names differ from the ones proposed in the #130 investigation. During implementation I found two problems with the _pre/_post naming from the issue:

  1. _pre was ambiguous. It could mean "modify arguments" or "gate execution" but couldn't do both. I need to block execution (rate limiting, circuit breakers) from the same hook that modifies arguments, not from a separate one.

  2. _post didn't say what you're filtering. _result is explicit.

I went with the WordPress core pre_* short-circuit pattern instead (pre_delete_post, pre_update_option, pre_get_shortlink). The filter returns the modified value to proceed or WP_Error to block. This is a pattern WordPress developers already know.

Test plan

  • All tests pass (npm run test:php)
  • PHPCS clean (npm run lint:php)
  • PHPStan Level 8 zero errors (npm run lint:php:stan)
  • pre_tool_call filter can modify arguments
  • pre_tool_call filter can block execution with WP_Error
  • tool_call_result filter can modify result
  • List filters can remove/add components
  • initialize_response filter can modify instructions
  • prompt_name filter can customize name
  • MCP Inspector smoke test: npx @modelcontextprotocol/inspector --cli --config .mcp.json --server mcp-adapter-default-npx --method tools/list

Ref #130

Add resolve_prompt_name() method following the same pattern as
resolve_tool_name() in RegisterAbilityAsMcpTool. Uses McpNameSanitizer
for normalization and McpValidator for post-filter validation.

Closes the naming parity gap — tools and resources already had naming
filters but prompts did not.

Ref #130
Add mcp_adapter_tools_list, mcp_adapter_resources_list, and
mcp_adapter_prompts_list filters. These fire after retrieving
components from the registry and before assembling the list result
DTO. Enables hiding components per user/role, adding dynamic
components, or reordering.

Ref #130
Filter fires after constructing InitializeResult and before returning.
Enables dynamic capabilities, custom instructions, and server info
modifications.

Ref #130
Pre-filter fires after permission check, before execution. Enables
argument transformation, context injection, and rate limiting.

Post-filter fires after execution, before DTO assembly. Enables
result transformation, PII redaction, and audit logging.

Ref #130
Pre-filter fires after permission check, before resource execution.
Post-filter fires after execution, before DTO conversion. Enables
access control, caching, content transformation, and audit logging.

Ref #130
Pre-filter fires after permission check, before prompt execution.
Post-filter fires after execution, before normalization. Enables
argument normalization, context injection, and message transformation.

Ref #130
Rename pre/post execution hooks to follow WordPress pre_* convention:
- mcp_adapter_tool_call_pre → mcp_adapter_pre_tool_call
- mcp_adapter_tool_call_post → mcp_adapter_tool_call_result
- mcp_adapter_resource_read_pre → mcp_adapter_pre_resource_read
- mcp_adapter_resource_read_post → mcp_adapter_resource_read_result
- mcp_adapter_prompt_get_pre → mcp_adapter_pre_prompt_get
- mcp_adapter_prompt_get_post → mcp_adapter_prompt_get_result

The pre_* filters now support WP_Error returns to short-circuit
execution, following the WordPress pre_delete_post pattern. This
enables rate limiting, circuit breakers, and custom permission
checks from filter callbacks.

Ref #130
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 20, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: galatanovidiu <ovidiu-galatan@git.wordpress.org>
Co-authored-by: gziolo <gziolo@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 98.52941% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 87.74%. Comparing base (31a4485) to head (9ad1c31).
⚠️ Report is 2 commits behind head on release/v0.5.0.

Files with missing lines Patch % Lines
...udes/Domain/Prompts/RegisterAbilityAsMcpPrompt.php 94.44% 1 Missing ⚠️
Additional details and impacted files
@@                 Coverage Diff                  @@
##             release/v0.5.0     #151      +/-   ##
====================================================
+ Coverage             87.62%   87.74%   +0.11%     
- Complexity             1218     1230      +12     
====================================================
  Files                    54       54              
  Lines                  3944     3990      +46     
====================================================
+ Hits                   3456     3501      +45     
- Misses                  488      489       +1     
Flag Coverage Δ
unit 87.74% <98.52%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@galatanovidiu galatanovidiu requested a review from gziolo March 20, 2026 14:23
@gziolo
Copy link
Copy Markdown
Member

gziolo commented Mar 23, 2026

A quick thought before I jump into details. There is a very similar feature request for the AI plugin:

I shared my thoughts in WordPress/ai#304 (comment), with the main takeaway being some of these filters would be better suited for WP core rather than the plugin.

Based only on the PR's description, I can see some similarities here. I can see why it also makes sense to add MCP adapter specific filters that tap into the execution flow (pre & result). At the same time, it's clear there is gap in WordPress core that we won't be able to fill before WordPress 7.1 (August 2026).

@ovidiu-galatan, I'd be curious to hear your perspective here.

Comment on lines +183 to +189
return CallToolResult::fromArray(
array(
'content' => array( ContentBlockHelper::text( $args->get_error_message() ) ),
'structuredContent' => null,
'isError' => true,
)
);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see it in a few places, so adding a simpler converter would be useful here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Extracted create_error_result() as a private method at the bottom of the class. Replaces all 4 identical CallToolResult::fromArray(... isError: true) constructions in call_tool(). See 5df485d.

*
* @since n.e.x.t
*
* @param mixed $result The raw execution result.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be worth explicitly mentioning WP_Error as a possible result here. In fact, it probably can be passed as $result in the first place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Updated the @param to mixed|\WP_Error on all three result filters (tool_call_result, resource_read_result, prompt_get_result). See d091cef.

* @param array<\WP\McpSchema\Server\Tools\DTO\Tool> $tools Array of Tool DTOs.
* @param \WP\MCP\Core\McpServer $server The MCP server instance.
*/
$tools = apply_filters( 'mcp_adapter_tools_list', $tools, $this->mcp );
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other filters, some basic validation is in place for what the filter returns. Here, this value is passed through, which might be fine. However, it's worth confirming it is an expected flow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added validate_filtered_list() to HandlerHelperTrait. If a filter returns a non-array, it logs a warning and falls back to the original unfiltered list. Applied to all three list filters. See 9ad1c31.

Copy link
Copy Markdown
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid set of hooks — good naming rationale, clean test coverage, and the pre_* / *_result pattern follows WordPress conventions well.

One question: the three mcp_adapter_pre_* filters handle WP_Error short-circuits differently:

  • mcp_adapter_pre_tool_callCallToolResult with isError=true (makes sense per MCP spec)
  • mcp_adapter_pre_resource_readMcpErrorFactory::internal_error()
  • mcp_adapter_pre_prompt_getMcpErrorFactory::permission_denied()

Was the permission_denied vs internal_error distinction intentional?

…cation

Consolidate 4 identical CallToolResult error constructions into
a private helper method at the bottom of the class. No behavior change.
Filter consumers should know that execute() may return WP_Error,
which passes through to the result filter before being handled.
Log a warning and fall back to the original array when a filter
callback returns a non-array from mcp_adapter_tools_list,
mcp_adapter_resources_list, or mcp_adapter_prompts_list.
@galatanovidiu
Copy link
Copy Markdown
Contributor Author

galatanovidiu commented Mar 26, 2026

Wasn't intentional. The permission_denied on the prompt pre-filter was a copy-paste from the nearby permission check. Both resources and prompts should probably use internal_error for pre-filter short-circuits since the WP_Error could be anything (rate limiting, maintenance mode, etc.), not specifically a permission failure.

Opened #152 to track this. I'd rather fix it in a follow-up PR since changing the error type is a behavior change that should be tested on its own.

@gziolo
Copy link
Copy Markdown
Member

gziolo commented Mar 26, 2026

Works for me. New utility methods added look great 👍🏻

@galatanovidiu
Copy link
Copy Markdown
Contributor Author

I looked at your comment on WordPress/ai#304. I think you're right that generic ability result filtering belongs in core (inside WP_Ability::execute()). That would close the gap between the existing wp_after_execute_ability action and what plugins actually need.

That said, I'd keep the MCP adapter's own result filters (mcp_adapter_tool_call_result, etc.) even after core adds its version. They operate at a different layer. A core filter would fire inside execute() and affect every consumer. The MCP filters fire after execution but before DTO assembly, so they're specific to what goes out over the MCP protocol.

Concrete example: you might want to redact sensitive data only in MCP responses (exposed to external LLMs) but not in the AI plugin's internal usage. That's only possible if both layers have their own hooks.

They'd compose naturally too: core filter runs first, MCP filter sees the already-filtered output. No conflict.

For the other hooks in this PR (pre_* short-circuits, list filters, initialize_response, prompt_name), those are all MCP-specific. They work with MCP domain objects and protocol DTOs, so there's no overlap with what core would provide.

@galatanovidiu galatanovidiu merged commit fb54ca4 into release/v0.5.0 Mar 26, 2026
19 of 22 checks passed
@galatanovidiu galatanovidiu deleted the feature/handler-filter-hooks branch March 26, 2026 11:40
@gziolo
Copy link
Copy Markdown
Member

gziolo commented Mar 26, 2026

I looked at your comment on WordPress/ai#304. I think you're right that generic ability result filtering belongs in core (inside WP_Ability::execute()). That would close the gap between the existing wp_after_execute_ability action and what plugins actually need.

Thank you for checking that and confirming my concerns. I will file a new Trac ticket at some point.

That said, I'd keep the MCP adapter's own result filters (mcp_adapter_tool_call_result, etc.) even after core adds its version. They operate at a different layer.

There is definitely a place for a specific filter like that. Even in WP core, you often have a pair of filters, one general purpose, and one that is tied to some subset of cases. If someone uses mcp_adapter_tool_call_result then it's clear they only care about MCP flow.

galatanovidiu added a commit that referenced this pull request Mar 26, 2026
* deps: Add wordpress/php-mcp-schema package (#119)

Adds the php-mcp-schema package providing type-safe DTOs
for MCP protocol specification 2025-11-25.

* refactor(infrastructure): Update error handling for MCP schema DTOs (#120)

- Update McpErrorFactory to produce JSONRPCErrorResponse DTOs
- Add FailureReason enum for structured failure tracking
- Update error handler implementations

* [3/6] Add domain contracts and utility classes for MCP components (#121)

* feat(domain): Add MCP component contracts and utilities

- Add McpComponentInterface for domain models
- Add utility classes: ArgumentNormalizer, ContentBlockHelper,
  AnnotationMapper, NameSanitizer, SchemaTransformer
- Update validators for schema compliance

* Update includes/Domain/Utils/McpValidator.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Unit/Domain/Utils/McpValidatorTest.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update includes/Domain/Utils/McpValidator.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update tests/Unit/Domain/Utils/McpValidatorTest.php

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* [4/6] Refactor domain models to use MCP schema DTOs (#122)

* refactor(domain): Update domain models to use MCP schema DTOs

- McpTool now wraps Tool DTO
- McpResource now wraps Resource DTO
- McpPrompt now wraps Prompt DTO
- Update all validators and ability converters

* refactor(domain): alias MCP DTOs and rename get_protocol_dto

- Alias schema DTOs as ToolDto/ResourceDto/PromptDto across domain components and tests
- Rename McpComponentInterface::get_component() to get_protocol_dto() and update call sites
- Clarify wrapper pattern in McpTool/McpResource/McpPrompt docs

* docs(domain): use FQNs in DTO phpdoc

* [5/6] Update core layer and built-in abilities for MCP schema (#123)

* refactor(core): Update core layer for MCP schema integration

- Update McpAdapter, McpServer, McpComponentRegistry
- Update built-in abilities for DTO responses
- Update plugin bootstrap sequence

* docs: Replace line number references with @see PHPDoc tags

Line numbers in docblock comments are fragile and become outdated
when code changes. Using @see tags provides stable references that
IDEs can resolve and that survive refactoring.

Addresses PR #123 review feedback from JasonTheAdams.

* docs: Remove redundant @phpstan-return annotations

The @phpstan-return tags were identical to the @return tags, making
them unnecessary. PHPStan reads native @return tags directly - the
@phpstan-* variants are only needed when the type differs from
documentation.

Addresses PR #123 review feedback from JasonTheAdams.

* refactor(core): inline discover abilities permission checks to remove indirection

* docs(core): add array element types to MCP PHPDocs / clarify transport and component array shapes

* refactor(core): use protocol DTO accessors and aliases

- Alias Prompt/Resource/Tool DTOs in core
- Switch registry/server to get_protocol_dto()
- Update core registry tests for DTO aliases

* [6/6] Complete MCP schema integration: handlers, transport, and CLI (#124)

* refactor(handlers): Complete MCP schema integration

- Update all request handlers for DTO handling
- Update HTTP and STDIO transports
- Add DTO serialization regression tests
- Full MCP 2025-11-25 compliance

* refactor: align handlers with protocol DTOs and FQN docblocks

* chore: fix unused DTO import

* test: fix prompt validator instance test

* fix: Use instanceof for exception categorization in RequestRouter

Replace exact class name matching with instanceof to properly categorize
subclass exceptions. OutOfBoundsException, UnexpectedValueException, and
other subclasses now map to their parent's category instead of 'unknown'.

* fix: Check wp_json_encode return value in PromptsHandler

wp_json_encode can return false on malformed UTF-8 or unencodable
data. The fallback was already handled in the invalid-type branch
but missing in the missing-type branch.

* fix: Add isset check for results in ToolsHandler image handling

Prevents PHP warning when a tool returns type=image without
a results key. Matches the defensive style used in the resource
handling branch above.

* refactor: Rename $message to $params in ToolsHandler::call_tool

Consistent with PromptsHandler::get_prompt and
ResourcesHandler::read_resource which already use $params.

* Add trim() to tool name in ToolsHandler::call_tool

Consistent with PromptsHandler and ResourcesHandler which
already trim the lookup key before resolving.

* Fix DEFAULT_MAX_SESSIONS doc comment

The constant is 32 but the filter PHPDoc said "Default 5".

* Remove dead error helper methods from HandlerHelperTrait

All handlers use McpErrorFactory directly. The trait's error helpers
(create_error_response, extract_error, missing_parameter_error,
permission_denied_error, internal_error, create_success_response)
were never called from production code. Removed along with their
tests in HandlerHelperTraitTest and ErrorResponseConsistencyTest.

* Fix formatting nitpicks across handlers and tests

- Remove double blank line in PromptsHandler
- Fix broken docblock in McpTransportContext (split constructor
  and property docblocks)
- Fix extra tab indentation in DtoSerializationRegressionTest
- Fix extra tab indentation in RequestRouterTest

* Extract DEFAULT_IMAGE_MIME_TYPE constant in ToolsHandler

Replaces hardcoded 'image/png' fallback with a named constant
for better readability.

* Remove unused $request_id from handler methods that never use it

Only call_tool, read_resource, and get_prompt need $request_id
because they pass it to McpErrorFactory for JSON-RPC error envelopes.
List, ping, and initialize handlers only return success DTOs, so the
parameter was dead weight.

* fix: add version to @deprecated tag and log unexpected handler return types

McpPromptBuilder @deprecated now includes version 0.5.0 per WordPress docs standard.
RequestRouter fallback block logs the actual unexpected type via error_handler for
server-side debugging before returning the generic error response.

* refactor: reformat test file to WordPress style and combine redundant null guards

RegistrationValidationTest reformatted from PSR-12/Allman to WordPress/K&R style
with tabs, matching all other test files. Two separate null-guard blocks in
RequestRouter combined into one — identical runtime behavior, less duplication.

* fix: pin php-mcp-schema to ^0.1.0 stable release

Replace dev branch reference (dev-feat/mcp-schema-2025-11-25) with the
proper v0.1.0 release constraint. The upstream feature branch was merged
and deleted; v0.1.0 is the first tagged release.

* Fix MCP HTTP response code compliance for notifications and sessions (#138)

* fix: add SESSION_NOT_FOUND error code and return HTTP 404 for expired sessions

MCP spec requires HTTP 404 for invalid/expired sessions, but the transport
was returning HTTP 200 via INVALID_PARAMS. Adds SESSION_NOT_FOUND (-32005)
error code, factory method, and HTTP status mapping. Updates
HttpSessionValidator to use the new error code. Also adds the 202 notification
null-check and an explicit comment for JSON-RPC response fall-through.

* test: update assertions for HTTP 202 notifications and 404 expired sessions

Updates 6 test assertions across 3 files to match the new MCP-compliant
HTTP response codes: notifications now return 202, expired/invalid sessions
return 404 with SESSION_NOT_FOUND error code instead of INVALID_PARAMS.

* refactor: import WP_Error instead of using fully qualified references (#140)

WP_Error is not an actual exception class — it doesn't extend
Throwable. The FullyQualifiedExceptions PHPCS rule was enforcing
FQN unnecessarily. Add WP_Error to ignoredNames and import it
via `use` statements across all namespaced files.

PHPDoc annotations retain \WP_Error per FullyQualifiedClassNameInAnnotation.

* refactor: adopt list<T> type annotations for sequential arrays (#139)

Replace array<int, T> with list<T> across all PHPDoc annotations for
sequential zero-indexed arrays. Propagate list<string> upstream through
McpAdapter, McpServer, and McpComponentRegistry. Add array_values() in
RegisterAbilityAsMcpPrompt::get_explicit_arguments() to guarantee
metadata arrays are proper lists.

Closes #127

* feat: implement MCP protocol version negotiation (#141)

* feat: add McpVersionNegotiator for protocol version negotiation

Pure Core layer class that negotiates the MCP protocol version between
client and server. If the client requests a supported version, the server
echoes it back; otherwise it falls back to the latest supported version.
Supports 2025-11-25, 2025-06-18, and 2024-11-05.

* test: add McpVersionNegotiator unit tests

Tests cover version negotiation (supported echo, unsupported fallback,
empty string), is_supported checks, and ordering invariant using data
providers for automatic adaptation when new versions are added.

* feat: negotiate protocol version in InitializeHandler

Read the client's requested protocolVersion from initialize params and
negotiate using McpVersionNegotiator. If supported, echo it back;
otherwise fall back to the latest version. Added
mcp_adapter_negotiated_protocol_version filter for overriding.

* feat: validate MCP-Protocol-Version header on HTTP requests

Extract Mcp-Protocol-Version header in HttpRequestContext and validate
it on non-initialize requests. Missing header is accepted; present but
unsupported returns HTTP 400 with JSON-RPC error code -32000. Matches
the TypeScript MCP SDK behavior per the 2025-11-25 spec.

* test: add protocol version header validation tests for HttpRequestHandler

Cover the validate_protocol_version_header() method and its call site
in process_jsonrpc_request(), resolving the Codecov patch coverage gap.
Tests verify: missing header accepted, supported version accepted,
unsupported version returns SERVER_ERROR, and initialize skips validation.

* fix: read session ID from user meta instead of applying rest_post_dispatch

WordPress trunk added _wp_connectors_validate_keys_in_rest() hooked to
rest_post_dispatch expecting 3 args. Our manual apply_filters call only
passed 1, causing ArgumentCountError on WP trunk CI. Reading the session
ID directly from user meta avoids the filter entirely.

* fix: preserve request id and return 400 for bad protocol header

* fix: add type guard for negotiated protocol version filter

WordPress apply_filters() has no return type guarantee — any hooked
callback could return a non-string value. Re-negotiate from scratch
when the filter returns an invalid type to ensure the server always
advertises a valid protocol version per MCP spec.

* fix: remove unnecessary protocol version filter and add version drift test

Remove `mcp_adapter_negotiated_protocol_version` filter from InitializeHandler.
Protocol version negotiation is a machine-to-machine contract with no legitimate
customization need. The filter created a footgun where a bad return value would
cause initialize to succeed but all subsequent requests to be rejected by the
transport layer's version validation.

Add canary test asserting SUPPORTED_PROTOCOL_VERSIONS[0] matches
McpConstants::LATEST_PROTOCOL_VERSION to catch drift between the adapter and
the php-mcp-schema vendor package.

* test: lock supported protocol versions to prevent silent test shrinkage

The data_supported_versions data provider derives fixtures from the
SUPPORTED_PROTOCOL_VERSIONS constant, so removing a version silently
reduces test coverage instead of failing. Add an explicit assertion on
the full expected set so changes require a deliberate test update.

* fix: bump version constant from 0.4.1 to 0.5.0

All three version declarations were still at 0.4.1 while v0.5.0
development was underway. This caused observability logs,
the WordPress admin plugin list, and the WP_MCP_VERSION constant
exposed to extensions to report the wrong version.

Updated:
- McpAdapter::VERSION class constant
- Plugin header Version field (read by WordPress core)
- WP_MCP_VERSION global constant

* fix: strip empty properties and convert objects to arrays in SchemaTransformer

SchemaTransformer now normalizes schemas before processing: converts objects
to arrays (from JSON decode cycles) and removes empty properties that would
serialize as JSON [] instead of the JSON object {} that MCP clients expect.

* fix: use string WP_Error code in SessionManager::get_session

Numeric 403 WP_Error code was inconsistent with WordPress conventions
and the rest of the codebase (mcp_tool_missing_name, mcp_resource_invalid_uri, etc.).
Changed to mcp_session_invalid_input. The 403 was not propagated anywhere —
get_session() is unused in production code.

* fix: use INVALID_REQUEST error code for HTTP 405 responses

HTTP 405 Method Not Allowed is a client error, not a server internal error.
Changed from internal_error (-32603) to invalid_request (-32600) which
semantically matches a wrong HTTP method. Updated assertions in both
unit and integration tests.

* chore: remove VCS repository for php-mcp-schema

Package is now published on Packagist, so Composer can resolve
it directly without the GitHub VCS repository entry.

* docs: v0.5.0 documentation improvements (#147)

* docs: rewrite architecture overview for v0.5.0 schema/adapter separation

Two-layer architecture (Schema Layer + Adapter Layer), McpComponentInterface,
DTO-based handler returns, new utility classes section, updated extension points.

* docs: create v0.5.0 migration guide for php-mcp-schema DTO integration

Breaking changes with before/after code, migration steps for custom transports,
handlers, components, error handling, and observability. Backward compat notes.

* docs: document tool naming transformation and McpNameSanitizer

Adds Tool Naming section to creating-abilities guide covering /→- transformation,
full sanitization pipeline, mcp_adapter_tool_name filter, and naming examples.

* docs: update README component tree, dependencies, and migration link for v0.5.0

Add McpComponentInterface, utility classes, FailureReason to architecture tree.
Add php-mcp-schema as required dependency. Add migration guide link.

* docs: add server selection guidance comparing default vs custom servers

Comparison table, layered discovery explanation, direct registration pattern,
filters for disabling/extending the default server.

* docs: document HTTP session requirement and Mcp-Session-Id flow

Session lifecycle (initialize, use header, terminate), curl examples,
error cases, configuration hooks, STDIO exemption, troubleshooting entry.

* docs: add migration guides section and upgrading navigation to docs index

Link v0.3.0 and v0.5.0 migration guides from docs/README.md navigation.

* fix: update McpErrorFactory usage in troubleshooting to use DTO return

Aligns with v0.5.0 DTO-based error responses documented in migration guide.

* docs: restructure v0.5.0 migration guide to lead with seamless upgrade

Most users see no breaking changes — public APIs, hooks, and factory methods
are all unchanged. Internal API changes moved to an Advanced section for
custom handler/transport implementers only.

* fix: address documentation review findings across all v0.5.0 docs

Architecture: fix ping return type, transport interface signature, json_text
signature, rest_ensure_response → WP_REST_Response, condense redundant sections.
Guides: fix protocol version in curl example, clarify tool naming scope,
add HTTP 200 error note, explain initialized notification.
README: add 4 missing files to component tree, fix dash formatting.
Troubleshooting: fix HTTP 400 reference to JSON-RPC error code.
Source: fix SessionManager PHPDoc timeout default (was 3600, actual DAY_IN_SECONDS).

* docs: apply WordPress sentence case heading standard and fix broken bold

Convert all headings to sentence case per WordPress Documentation Style Guide.
Fix broken bold markup on creating-abilities.md line 12.

* fix: address Copilot review findings on v0.5.0 docs

Tool names now show MCP-sanitized form (hyphens) with a note explaining
the ability registration name (slashes) vs MCP tool name distinction.
Session flow clarifies GET/SSE is reserved for future support. Protocol
version corrected to 2025-11-25 in troubleshooting example. Interface
snippet aligned with actual McpRestTransportInterface signature.

* Add 11 handler filter hooks for extensibility (#151)

* feat: add mcp_adapter_prompt_name filter for naming parity

Add resolve_prompt_name() method following the same pattern as
resolve_tool_name() in RegisterAbilityAsMcpTool. Uses McpNameSanitizer
for normalization and McpValidator for post-filter validation.

Closes the naming parity gap — tools and resources already had naming
filters but prompts did not.

Ref #130

* feat: add list filters for tools, resources, and prompts

Add mcp_adapter_tools_list, mcp_adapter_resources_list, and
mcp_adapter_prompts_list filters. These fire after retrieving
components from the registry and before assembling the list result
DTO. Enables hiding components per user/role, adding dynamic
components, or reordering.

Ref #130

* feat: add mcp_adapter_initialize_response filter

Filter fires after constructing InitializeResult and before returning.
Enables dynamic capabilities, custom instructions, and server info
modifications.

Ref #130

* feat: add mcp_adapter_tool_call_pre and _post filters

Pre-filter fires after permission check, before execution. Enables
argument transformation, context injection, and rate limiting.

Post-filter fires after execution, before DTO assembly. Enables
result transformation, PII redaction, and audit logging.

Ref #130

* feat: add mcp_adapter_resource_read_pre and _post filters

Pre-filter fires after permission check, before resource execution.
Post-filter fires after execution, before DTO conversion. Enables
access control, caching, content transformation, and audit logging.

Ref #130

* feat: add mcp_adapter_prompt_get_pre and _post filters

Pre-filter fires after permission check, before prompt execution.
Post-filter fires after execution, before normalization. Enables
argument normalization, context injection, and message transformation.

Ref #130

* refactor: rename execution hooks and add WP_Error short-circuit

Rename pre/post execution hooks to follow WordPress pre_* convention:
- mcp_adapter_tool_call_pre → mcp_adapter_pre_tool_call
- mcp_adapter_tool_call_post → mcp_adapter_tool_call_result
- mcp_adapter_resource_read_pre → mcp_adapter_pre_resource_read
- mcp_adapter_resource_read_post → mcp_adapter_resource_read_result
- mcp_adapter_prompt_get_pre → mcp_adapter_pre_prompt_get
- mcp_adapter_prompt_get_post → mcp_adapter_prompt_get_result

The pre_* filters now support WP_Error returns to short-circuit
execution, following the WordPress pre_delete_post pattern. This
enables rate limiting, circuit breakers, and custom permission
checks from filter callbacks.

Ref #130

* refactor: extract ToolsHandler::create_error_result() to reduce duplication

Consolidate 4 identical CallToolResult error constructions into
a private helper method at the bottom of the class. No behavior change.

* docs: document WP_Error as possible value in result filter params

Filter consumers should know that execute() may return WP_Error,
which passes through to the result filter before being handled.

* feat: add return type validation for list filter hooks

Log a warning and fall back to the original array when a filter
callback returns a non-array from mcp_adapter_tools_list,
mcp_adapter_resources_list, or mcp_adapter_prompts_list.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@gziolo
Copy link
Copy Markdown
Member

gziolo commented Mar 30, 2026

To close the loop and seek additional feedback, I opened Abilities API: Add execution lifecycle filters to WP_Ability methods in WordPress core.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants