Skip to content

feat(website): a website env may be an MCP server too, its tools declared on its card (FD-4683) - #508

Open
williamloo-scale wants to merge 13 commits into
mainfrom
wloo/website-mcp
Open

williamloo-scale wants to merge 13 commits into
mainfrom
wloo/website-mcp

Conversation

@williamloo-scale

@williamloo-scale williamloo-scale commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

A website env can now also be an MCP server. A site gives agents tools beside its pages, its card declares them, and the gateway serves them as it does an MCP env's. Stacked on #429 (the Notes example, rebased onto main after #428 merged). The Notes example skips the new mcp check, since it serves no tools.

What changes

  • SDK (agentenv_protocol.website):
    • A WebsiteEnvironment with @tool or @resource methods serves them at /mcp (streamable HTTP, needs the mcp package). They run in the site's own process, beside its pages, @api routes and data plane, so a tool call changes what the pages show and what data/get returns, and it reads the universe clock (self.now()).
    • The card declares the endpoint as its mcp interface (additionalInterfaces) and lists the tools under capabilities.tools.
    • A site without tools serves no /mcp and declares no interface. serves_mcp() tells you which kind a site is.
  • Gateway:
    • At startup the gateway reads each site's card. When a card declares an mcp interface, the gateway adds the site as an internal MCP server, so agents see its tools with the rest, called with the caller's role in ToolContext.
    • This is decided from the card, not from stored config, and needs no compose changes. A site that answers no card, or declares no interface, is left out.
  • Reserved paths: site_reserved_paths now includes a card's interfaces, so the browsing route (/website/<name>/) never reaches a site's /mcp, as with the data plane.
  • Conformance: a new mcp check. When the card declares the interface, the endpoint must answer initialize and tools/list must name exactly the card's tools. A site without one skips the check.
  • README: a "Websites with tools" section.

Tests

  • packages/agentenv-protocol/tests/test_website_mcp.py:
    • the card declares /mcp and its tools;
    • a tool call changes what the site's API and data/get return, with the caller's role and X-Universe-Now;
    • a site without tools serves no /mcp;
    • conformance passes with tools and skips without them;
    • the mcp check fails on a tools mismatch or a missing endpoint, and sends nothing when the interface names another server.
  • tst/unit/env/gateway/website_mcp_test.py:
    • finding sites from their cards (retrying a site that isn't up yet, skipping names already taken, no card, other transports);
    • interfaces counted as reserved paths;
    • end to end on loopback, a real site and a real gateway: the gateway lists and calls the site's tool with the caller's role, /website/shop/mcp answers 404, and /svc/shop/agentenv data/get sees the order the tool placed.
  • Full suite: 13,337 passed and 7 failed.
    • 6 of the 7 fail on unmodified main on this machine too: cua_driver_test, test_disk_recipe and two in test_optional_extras_stay_optional.
    • The 7th, process_scope_socket_test::test_an_armed_time_trigger_keeps_firing_after_a_client_leaves, is a timing test that failed under full-suite load and passed 5 out of 5 on its own. tst/unit/env/gateway passes on its own (810 tests).
    • ruff is clean.

🤖 Generated with Claude Code

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no blocking issues remain.

Summary

Websites can serve MCP tools beside their pages. The gateway discovers those tools from each site's card and keeps MCP endpoints out of browser routes.

  • Website environments can serve tools beside their pages.
  • The gateway serves tools from websites that declare an MCP endpoint.
  • Website conformance checks compare MCP tools with the site card.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Card[Website card] --> Discovery[Gateway discovery]
  Discovery --> Tools[Gateway tools]
  Tools --> MCP[Website MCP endpoint]
  Service[Service route] --> Stamp[Gateway writes caller role]
  Stamp --> MCP
  Browser[Browser route] --> Block[Declared MCP path returns 404]
Loading

Reviews (7) · Last reviewed commit: "fix(website): a full MCP URL on a site b..." · Reviewed by Greptile

@williamloo-scale
williamloo-scale requested a review from a team as a code owner November 5, 2026 21:42
Comment thread src/agent_env/env/gateway/entrypoint.py Resolved
Comment thread src/agent_env/env/gateway/entrypoint.py Resolved
williamloo-scale and others added 6 commits November 5, 2026 14:52
…ses the conformance check (FD-4683)

This PR's change, rebased as one commit onto #428.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, and adds a note once per submit (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sign-in fails, and reloads itself at its slash-ending address (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hrough data/get, not /go (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s, settles a save the server accepted, and reloads itself on its own origin (FD-4683)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…through website/v1's ready alone (FD-4683)

The SDK on main keeps no store and serves no health path, so Notes holds its users and notes itself, starting from
data/seed.json, serves them through @reset_data, @add_data and @get_data, and names its acting user with
acting_user(). Its readiness is website/v1's ready (@ready_check seeded); env.toml and the image's health check no
longer name /healthz, which the compose health check now reads as the card.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@williamloo-scale
williamloo-scale changed the base branch from main to wloo/website-notes-example November 5, 2026 22:58
Comment thread src/agent_env/env/gateway/entrypoint.py Resolved
williamloo-scale and others added 4 commits November 5, 2026 15:06
…oncurrent writes keep every note (FD-4683)

Plain def handlers run in worker threads, so two creates could choose one id, and a create could append to a list a
delete was replacing. Every read and write of the site's data is under one lock, ids chosen and notes inserted
together, and the handlers answer copies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ared on its card (FD-4683)

- A WebsiteEnvironment with @tool or @resource methods serves them at /mcp (streamable HTTP, the mcp package) beside
  its pages, @api routes and data plane, in the site's process and under its clock; its card declares that endpoint as
  its mcp interface and lists the tools. A site without them serves no /mcp and declares none (serves_mcp()).
- The gateway reads each site's card when it starts and serves the tools of one whose card declares an mcp interface
  as it does an MCP env's, with the caller's role in ToolContext. A site's interfaces are among the paths its card
  advertises, so its browsing route never reaches its /mcp.
- Conformance checks a declared mcp endpoint: it answers initialize and lists the card's tools; a site without one
  skips the check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e, sites are found at once, and conformance reads every page

- A call to a site's MCP endpoint through /svc/<name>/ gets the gateway's role in its JSON-RPC _meta, as an MCP
  server's does, at the endpoint its card declares (site_mcp_endpoints), so a caller can't claim another role there.
- The gateway asks every site for its card at once, so a site that never comes up leaves another its own retries.
- Conformance reads every page of tools/list, fails a listed tool the card doesn't name, and warns on a card's tool
  it doesn't list, as a hidden-by-default one is listed to no caller.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread src/agent_env/env/gateway/gateway.py Resolved
…wever its card writes it, and conformance reads whole events

- The gateway takes a card's MCP endpoint written as a full URL when it names the site's own server, joining a path
  under the site's address as before, and leaves out, saying so, one on another server.
- A site's reserved paths come from each URL its card advertises, a full one or one with a query as well as a bare
  path, so its browsing route never reaches /mcp however the card writes it.
- Conformance reads an event stream's events whole, their data lines joined, and takes the reply to its own request,
  as ToolSession does, so a reply split over lines or beside notifications is read right.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread src/agent_env/env/gateway/entrypoint.py Resolved
…d the /svc role stamp matches it by address

A card may write its own server differently from the gateway's base (https://shop.test:443/mcp beside
https://shop.test). Discovery now returns the endpoint under the base's own spelling, and site_mcp_endpoints compares
parsed scheme, host and port rather than a string prefix, so a call through /svc/<name>/ to that endpoint still gets
the gateway's role in its _meta.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread src/agent_env/env/gateway/entrypoint.py Resolved
… own path

A card's full URL already names its path from the server's root, so the gateway keeps that path on the site's scheme
and host instead of joining it under the site's base path, which a prefixed base doubled.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Base automatically changed from wloo/website-notes-example to main November 6, 2026 00:26

This branch has not been deployed

No deployments
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