Skip to content

Add reflex-webmcp package with WebMCPPlugin - #7007

Open
FarhanAliRaza wants to merge 2 commits into
reflex-dev:mainfrom
FarhanAliRaza:webmcp-plugin
Open

Add reflex-webmcp package with WebMCPPlugin#7007
FarhanAliRaza wants to merge 2 commits into
reflex-dev:mainfrom
FarhanAliRaza:webmcp-plugin

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

New workspace package reflex-webmcp with a WebMCPPlugin that exposes backend Reflex events already bound to components as WebMCP site tools — no separate tool definitions or JS handlers.

from reflex_webmcp import WebMCPPlugin
config = rx.Config(app_name="my_app", plugins=[WebMCPPlugin()])
  • At compile time, walks each page's component tree and finds backend EventSpecs in event chains (including rx.foreach rows).
  • One tool per unique handler, named reflex_<StateClass>_<handler>; description from the docstring; JSON input schema from parameter annotations (TypedDict forms → field-level schema, Literal → enum).
  • Arguments fixed by the component (on_click=State.pick("x")) are baked in and dropped from the schema; distinct fixed instances get a digest-suffixed name. Runtime Var args stay in the schema for the agent.
  • Execution queues the same ReflexEvent through addEvents, so auth, event processing, input conversion and event actions (debounce/throttle) are unchanged.
  • Skips frontend-only events, lifecycle triggers, upload handlers, dynamic event vars and variadic handlers. Browsers without WebMCP are unaffected.

Also: workspace/dev-dep/pyright wiring, dispatch_release input for the new package, docs in docs/api-reference/plugins.md, news fragment in the package.

Test plan

  • uv run pytest tests/units/reflex_webmcp (15 tests)
  • uv run ruff check / ruff format --check on the package and tests
  • uv run pyright packages/reflex-webmcp/src tests/units/reflex_webmcp
  • uv run reflex-release sync — no diff
  • Manually exercised with a Konva meme-maker example app: 17 tools registered, driven via document.modelContext.listTools().execute(...) in Chrome

Review in cubic

New workspace package `reflex-webmcp` providing `WebMCPPlugin`. At compile
time the plugin walks each page's component tree, finds backend EventSpecs
bound to component triggers (including rows rendered by rx.foreach), and
registers one WebMCP tool per unique handler via `document.modelContext`.

- Tool name: `reflex_<StateClass>_<handler>`; description from the handler
  docstring; JSON input schema from parameter annotations (TypedDict forms
  give field-level schemas, Literal gives enums).
- Arguments fixed by the component (`on_click=State.pick("x")`) are baked
  into the tool and removed from its schema; distinct fixed instances get a
  digest-suffixed name. Var args (foreach row ids) stay in the schema.
- Tool execution queues the same ReflexEvent through addEvents, so auth,
  event processing and input conversion are unchanged.
- Frontend-only events, lifecycle triggers, upload handlers and variadic
  handlers are skipped; browsers without WebMCP are unaffected.

Also wires the package into the workspace, dev deps, pyright paths, the
dispatch_release workflow, and documents it in the plugins API reference.

Claude-Session: https://claude.ai/code/session_01QcxjeQ2x7mujrrNuJJo39S
@FarhanAliRaza
FarhanAliRaza requested review from a team and Alek99 as code owners August 30, 2026 18:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-30T18:29:40.087613Z 5a700c2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@greptile-apps

greptile-apps Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds the reflex-webmcp workspace package, which discovers component-bound backend events during compilation and exposes them as browser WebMCP tools.

  • Generates tool descriptions and JSON input schemas from event-handler metadata.
  • Preserves fixed arguments and event actions when dispatching through the existing Reflex event pipeline.
  • Adds package, release-workflow, logging, documentation, lockfile, and unit-test integration.

Confidence Score: 3/5

The PR does not yet appear safe to merge because route-specific tools remain registered after navigation and distinct qualified handlers can still collide under one global tool name.

The current implementation still performs document-global registration without route-unmount cleanup, and its runtime deduplication key omits the handler's qualified State identity, leaving both previously reported functional failures reachable.

Files Needing Attention: packages/reflex-webmcp/src/reflex_webmcp/init.py

Important Files Changed

Filename Overview
packages/reflex-webmcp/src/reflex_webmcp/init.py Implements event discovery, schema generation, tool registration, and event dispatch; the previously reported registration-lifecycle and qualified-name identity issues remain unresolved.
tests/units/reflex_webmcp/test_init.py Covers event discovery, schemas, fixed payloads, event actions, deduplication, unsupported handlers, and memoized foreach bodies.
packages/reflex-webmcp/pyproject.toml Defines the new package's metadata, build backend, Python requirement, and runtime dependencies.
.github/workflows/dispatch_release.yml Adds reflex-webmcp as an optional package in the release-dispatch workflow.
packages/reflex-base/src/reflex_base/utils/log.py Routes the new package logger through the existing Reflex package logger hierarchy.

Reviews (2): Last reviewed commit: "Register reflex_webmcp in PACKAGE_LOGGER..." | Re-trigger Greptile

Comment on lines +389 to +393
globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")] ??= new Set();
if (!globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")].has({name})) {{
globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")].add({name});
try {{
await document.modelContext.registerTool({{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Registrations outlive their routes

When client-side navigation unmounts a route-specific component tree, its tool remains in the document-global registry with an executable closure, causing agents to keep discovering and dispatching backend actions that are no longer represented by the active page.

Knowledge Base Used:


fixed_payload = _fixed_payload(event_spec)
input_schema = _specialize_schema(handler_schema, fixed_payload)
generated_name = _tool_name(state.__name__, handler)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Qualified state names collide

When different modules define bound State classes and handlers with the same class and method names, _tool_name collapses their distinct qualified backend identities into one global tool name. The second registration is suppressed, causing its action to disappear while the surviving tool dispatches the other state's event.

Knowledge Base Used: State management and event processing

@codspeed-hq

codspeed-hq Bot commented Aug 30, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 32 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing FarhanAliRaza:webmcp-plugin (dd8c008) with main (dd96aea)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="pyproject.toml">

<violation number="1" location="pyproject.toml:112">
P2: The new workspace package `reflex-webmcp` (import module `reflex_webmcp`) was added to the workspace, but its import module was not added to `[tool.coverage.run] source`. Every other internal package appears there (`reflex_docgen`, `reflex_release`, `reflex_site_shared`, etc.), so its tests will not be measured and the package's lines won't contribute to the 72% `fail_under` coverage gate. Add `"reflex_webmcp"` to the `source` list.</violation>
</file>

<file name="packages/reflex-webmcp/src/reflex_webmcp/__init__.py">

<violation number="1" location="packages/reflex-webmcp/src/reflex_webmcp/__init__.py:369">
P2: When two state classes in different modules share a class and handler name, this generates the same WebMCP name and the global deduplication set skips the second tool. Use a sanitized qualified state name or add a collision suffix.</violation>

<violation number="2" location="packages/reflex-webmcp/src/reflex_webmcp/__init__.py:393">
P2: When a route component is unmounted, this registration remains in `document.modelContext` because it passes no `AbortSignal` and never aborts a controller. Register each tool with a route-scoped signal and abort it during route cleanup so agents cannot invoke stale handlers.</violation>
</file>

<file name="docs/api-reference/plugins.md">

<violation number="1" location="docs/api-reference/plugins.md:88">
P3: The example code block calls `search_products(query, limit=limit)` but never defines or imports it, so a user who copies this snippet (and others in the PR) gets a `NameError` on first run. Every other plugin example in this file is self-contained. Either define a small `search_products` helper in the block or note that it is a stand-in for the app's own lookup, so the "no separate tool definitions" example is actually runnable.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread pyproject.toml
"reflex-docgen",
"reflex-release",
"reflex-site-shared",
"reflex-webmcp",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The new workspace package reflex-webmcp (import module reflex_webmcp) was added to the workspace, but its import module was not added to [tool.coverage.run] source. Every other internal package appears there (reflex_docgen, reflex_release, reflex_site_shared, etc.), so its tests will not be measured and the package's lines won't contribute to the 72% fail_under coverage gate. Add "reflex_webmcp" to the source list.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pyproject.toml, line 112:

<comment>The new workspace package `reflex-webmcp` (import module `reflex_webmcp`) was added to the workspace, but its import module was not added to `[tool.coverage.run] source`. Every other internal package appears there (`reflex_docgen`, `reflex_release`, `reflex_site_shared`, etc.), so its tests will not be measured and the package's lines won't contribute to the 72% `fail_under` coverage gate. Add `"reflex_webmcp"` to the `source` list.</comment>

<file context>
@@ -109,6 +109,7 @@ dev = [
   "reflex-docgen",
   "reflex-release",
   "reflex-site-shared",
+  "reflex-webmcp",
   "ruff",
   "selenium",
</file context>


fixed_payload = _fixed_payload(event_spec)
input_schema = _specialize_schema(handler_schema, fixed_payload)
generated_name = _tool_name(state.__name__, handler)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When two state classes in different modules share a class and handler name, this generates the same WebMCP name and the global deduplication set skips the second tool. Use a sanitized qualified state name or add a collision suffix.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-webmcp/src/reflex_webmcp/__init__.py, line 369:

<comment>When two state classes in different modules share a class and handler name, this generates the same WebMCP name and the global deduplication set skips the second tool. Use a sanitized qualified state name or add a collision suffix.</comment>

<file context>
@@ -0,0 +1,511 @@
+
+    fixed_payload = _fixed_payload(event_spec)
+    input_schema = _specialize_schema(handler_schema, fixed_payload)
+    generated_name = _tool_name(state.__name__, handler)
+    event_actions = event_spec.event_actions
+    if fixed_payload or event_actions:
</file context>

if (!globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")].has({name})) {{
globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")].add({name});
try {{
await document.modelContext.registerTool({{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: When a route component is unmounted, this registration remains in document.modelContext because it passes no AbortSignal and never aborts a controller. Register each tool with a route-scoped signal and abort it during route cleanup so agents cannot invoke stale handlers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/reflex-webmcp/src/reflex_webmcp/__init__.py, line 393:

<comment>When a route component is unmounted, this registration remains in `document.modelContext` because it passes no `AbortSignal` and never aborts a controller. Register each tool with a route-scoped signal and abort it during route cleanup so agents cannot invoke stale handlers.</comment>

<file context>
@@ -0,0 +1,511 @@
+  if (!globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")].has({name})) {{
+    globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")].add({name});
+    try {{
+      await document.modelContext.registerTool({{
+        name: {name},
+        description: {description},
</file context>


def search_catalog(self, query: str, limit: int = 10):
"""Search the product catalog visible on this page."""
self.results = search_products(query, limit=limit)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The example code block calls search_products(query, limit=limit) but never defines or imports it, so a user who copies this snippet (and others in the PR) gets a NameError on first run. Every other plugin example in this file is self-contained. Either define a small search_products helper in the block or note that it is a stand-in for the app's own lookup, so the "no separate tool definitions" example is actually runnable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/api-reference/plugins.md, line 88:

<comment>The example code block calls `search_products(query, limit=limit)` but never defines or imports it, so a user who copies this snippet (and others in the PR) gets a `NameError` on first run. Every other plugin example in this file is self-contained. Either define a small `search_products` helper in the block or note that it is a stand-in for the app's own lookup, so the "no separate tool definitions" example is actually runnable.</comment>

<file context>
@@ -65,6 +65,62 @@ The sitemap configuration supports the following options:
+
+    def search_catalog(self, query: str, limit: int = 10):
+        """Search the product catalog visible on this page."""
+        self.results = search_products(query, limit=limit)
+
+
</file context>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a700c2a9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


fixed_payload = _fixed_payload(event_spec)
input_schema = _specialize_schema(handler_schema, fixed_payload)
generated_name = _tool_name(state.__name__, handler)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include the full state identity in tool names

When two page modules define the same class and handler names (for example, the common State.submit pattern), their backend event names remain distinct because Reflex includes the module-qualified state name, but this call reduces both tools to reflex_State_submit. The document-global name set then keeps only the first registration, so invoking the tool on the other page can dispatch the wrong module's handler. Derive the tool name from the qualified state/event identity instead of state.__name__.

Useful? React with 👍 / 👎.

Comment on lines +389 to +393
globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")] ??= new Set();
if (!globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")].has({name})) {{
globalThis[Symbol.for(\"reflex.webmcp.registeredTools\")].add({name});
try {{
await document.modelContext.registerTool({{

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Scope registered tools to the active route

In a multi-route Reflex app, client-side navigation reuses the same document, but these top-level document.modelContext registrations are never removed. Once a route module runs, its tools and callbacks remain available after navigating away, allowing agents to invoke handlers that are no longer present on the active page; the global set also prevents a later route from replacing an identically named registration. Register and unregister tools with the route/page lifecycle rather than retaining them for the document lifetime.

Useful? React with 👍 / 👎.

Comment on lines +159 to +160
if origin is Annotated:
return _annotation_schema(args[0])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Unwrap TypedDict field requirement annotations

When a TypedDict uses Required[T] or NotRequired[T]—the standard way to mix required and optional fields—get_origin() returns the requirement wrapper, which falls through here to {}. The generated object schema records whether the key is required but loses its advertised field type entirely, so agents receive an unconstrained value schema. Treat these wrappers like Annotated and recurse into their first argument.

Useful? React with 👍 / 👎.

Fixes test_every_logging_package_root_is_registered in CI.

Claude-Session: https://claude.ai/code/session_01QcxjeQ2x7mujrrNuJJo39S
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.

1 participant