MCP Toolsets Runtime
OfficialREADME.md
# mcp-toolsets-runtime
[](https://pypi.org/project/mcp-toolsets-runtime/)
[](https://www.npmjs.com/package/@developmentseed/mcp-view)
The shared runtime for [MCP Toolsets](https://github.com/developmentseed/mcp-toolsets).
Both `developmentseed/mcp-toolsets` and downstream repos generated from it
install this package instead of each carrying their own copy of the runtime.
## What's in here
One Python distribution (`mcp-toolsets-runtime`) exposing five top-level
modules, plus the view-side JS bridge:
| Module | What it is |
| --- | --- |
| `mcp_runtime` | Discovers a toolset's LangChain tools (`TOOLS`) and serves them as an MCP server; serves UI views (`VIEWS`) as `ui://` resources; derives server `instructions` from `CREDENTIAL_HEADERS`; advertises what each tool publishes into session state, and which parameters a model may not write (`NotAuthored`). Entry points: `mcp-serve` (one toolset), `mcp-serve-local` (several at once, for local dev), `mcp-index`. |
| `mcp_state` | Session state for *any* agent driving MCP tools: the `tool_state` namespace, `StateCaptureMiddleware` (moves large payloads out of the transcript), `inspect_state` (the model reads one on demand, including as it stood at an earlier turn), and `bind_injected` (fills declared parameters from state, and offers `@state:<key>` handles on the rest). A filled parameter leaves a receipt, so a value the model never saw can still be traced to the tool that published it. Works against unmodified third-party servers. Requires the `[state]` extra. |
| `mcp_cli` | Typer CLI to list and call tools on a running MCP service. Entry point: `mcp-cli`. |
| `mcp_toolset` | Scaffolds a new toolset in a consumer repo (`mcp-toolset new [--with-ui] <name>`), wired to this package + the npm view bridge. |
| `mcp_agent` | Example Chainlit chat agent that discovers MCP servers behind an index URL and drives their tools, with `mcp_state` wired in (`MCP_AGENT_STATE=0` to opt out). Conversations are checkpointed per `thread_id` — in-process by default, PostgreSQL via `MCP_AGENT_CHECKPOINT` + the `[checkpointing-postgres]` extra. Ships the Chainlit host element `elements/McpView.jsx`. Entry points: `mcp-agent`, `mcp-agent-web`. `mcp_agent.main` (`build_agent`, `run_turn`), `mcp_agent.streaming` (`stream_turn`, the same turn yielded as it happens) and `mcp_agent.host` — the UI-framework-free helpers a host of its own needs (view bundles and props, and the tool-step arguments session state filled in) — need the `[agent]` extra. `mcp_agent.web`, the Chainlit host, needs `[web]` on top. |
| `mcp_agent_api` | The agent over HTTP. `mcp_agent_api.events` turns one turn into [AG-UI](https://github.com/ag-ui-protocol/ag-ui) events — tokens, tool calls, and the two things AG-UI has no vocabulary for: where each tool's arguments came from and which `ui://` view renders its result, both as `ACTIVITY_*` messages carrying a rendered `display` line beside their fields. Imports no FastAPI. `mcp_agent_api.routes` is an `APIRouter` over a built agent — `POST /runs` streams that turn as SSE, and four read routes serve what the stream deliberately leaves out: the thread's transcript, its turns with the state each ended holding, a session-state payload in full (`?turn=N` for the value as it stood then, which the checkpointer has kept all along), and a `ui://` view bundle. `mcp_agent_api.app` closes the stack for a deployment with no application of its own: `create_app(build=…)` puts a lifespan, a checkpointer, CORS and two health probes around those routes, and a module-level `app` serves under `uvicorn mcp_agent_api.app:app`. A sixth route, `GET /connections`, says what the agent connected to and which credential headers it wants, which is what a client needs before there is a conversation. `mcp_agent_api.ui` serves the **bundled web client** (below) beside all of it. Requires the `[api]` extra. |
| `@developmentseed/mcp-view` (`js/mcp-view`) | The view-side `ui/*` postMessage bridge a toolset UI imports (`onData` / `sendMessage`). Published to npm separately. |
### The toolset plugin contract
`mcp_runtime` discovers a toolset purely by convention — a `<toolset>.tools`
module exporting:
- `TOOLS` — a non-empty list of LangChain tools that return a `ToolResult`.
- `VIEWS` *(optional)* — `{tool_name: view_id}`, with a built bundle at
`<package>/views/<view_id>.html`.
- `CREDENTIAL_HEADERS` *(optional)* — header names the tools read off the
transport; used to derive the model-facing auth hint.
Every data key of a `ToolResult` — every field but `message` — is a value the
tool publishes. An `mcp_state` client captures each into session state under
`<toolset>/<tool>/<field>` and lets a later tool be pointed at it by that key,
so a large value — a geometry, an item collection — moves from the tool that
produced it to the tool that needs it *without passing through the model*.
Producer and consumer may be different toolsets on different servers; the key
is the only thing they share, which is why **a data key is a public name**.
A tool may also tag a parameter `NotAuthored`, which says only that a model
must not write the value — no type, nothing for another toolset to agree with.
An `mcp_state` client narrows that parameter until the only thing it accepts is
a reference to a value some tool already produced; a client that has never
heard of any of this is unaffected.
Keeping a value out of the context is client-side work, so an external MCP host
does none of it: served to Claude.ai or ChatGPT, a toolset behaves like
any other. Tag for the agents that understand it, and size tool returns for the
clients that don't.
Tagging is an accelerator, not a requirement: `mcp_state` moves values across
**unmodified third-party MCP servers** too, by capturing large returns on size
and letting the model point a parameter at one with an `@state:<key>` handle.
What the tag buys is that the parameter leaves the model's schema entirely.
Treat `ToolResult`, `NotAuthored`, and the `ui/*` wire protocol as **public API**. The
state contract, worked through as sequence diagrams — including the trust
assumption it rests on — is in
**[docs/SESSION-STATE.md](./docs/SESSION-STATE.md)**, with a runnable version
of the whole thing, against a third-party server included, in
**[examples/session-state/](./examples/session-state/)** (`uv run python
examples/session-state/demo.py` — no API key needed). The same machinery on the
wire, driven over HTTP by the client the wheel ships, is in
**[examples/agui-events/](./examples/agui-events/)** — tokens streaming, tool
calls and receipts in the order they arrive, and a state panel whose values are
a fetch away rather than on the wire.
## The bundled web client
`[api]` installs a page as well as an API. `mcp_agent_api.app` serves it at the
root, so a container running `uvicorn mcp_agent_api.app:app` is a working chat
over the toolsets behind `MCP_URL` — the transcript, tool calls and receipts as
they happen, the session-state panel, and `ui://` views in their frames. No
Node runs in the image and no front end is copied into the deployment.
What a deployment says about it is text and one colour, read from the
environment at startup:
| | |
| --- | --- |
| `MCP_AGENT_UI_TITLE` | the name in the header and the browser tab |
| `MCP_AGENT_UI_TAGLINE` | one line beside it |
| `MCP_AGENT_UI_GREETING` | the opening paragraph; unset, the page says what `GET /connections` reports |
| `MCP_AGENT_UI_EXAMPLES` | questions offered as buttons, one per line (or a JSON array) |
| `MCP_AGENT_UI_ACCENT` | a CSS colour |
Anything structural is a change to the client, whose source is
[`js/agent-ui`](./js/agent-ui). It talks to the six routes in
`mcp_agent_api.routes` and nothing else, so a host that mounts `create_router`
into an application of its own serves the same client with
`mount_ui(app, api="/api")`; what forces a fork is diverging from those routes,
not from the application around them. `create_app(ui=False)` turns the page off
for a deployment with a front end of its own.
## Install
From PyPI — see the badge above for the current release:
```bash
# base: runtime + cli (lean, for tool-serving images)
pip install mcp-toolsets-runtime
# session state, for wiring it into an agent of your own
pip install "mcp-toolsets-runtime[state]"
# the agent — build_agent, run_turn, stream_turn and the host helpers
pip install "mcp-toolsets-runtime[agent]"
# the bundled Chainlit web host, on top of the agent
pip install "mcp-toolsets-runtime[web]"
# the agent over HTTP as AG-UI events, plus the web client that renders them
# — an alternative to [web], not a layer
pip install "mcp-toolsets-runtime[api]"
```
`[state]`, `[agent]` and `[web]` are a chain, so name only the outermost you
need. `[api]` sits beside `[web]` on top of `[agent]`: a deployment serving the
API does not install Chainlit, and one serving the chat does not install AG-UI.
With uv, as a consumer — an ordinary dependency, no source override:
```toml
dependencies = ["mcp-toolsets-runtime[web]"]
```
Imports are unchanged from the old workspace packages: `from mcp_runtime.server
import build_server`, etc. `uv.lock` pins whatever resolved, so upgrading is
`uv lock --upgrade-package mcp-toolsets-runtime`. The package is pre-1.0, where
a minor release may break — bound it at the next minor in your own
`pyproject.toml` if you'd rather take those deliberately.
**Consuming this package** — the plugin contract, serving toolsets, wiring up UI
views (including `mcp-agent install-elements` and the npm bridge), wiring session
state into your own agent, serving that agent over HTTP, and migrating off the
in-repo workspace: see
**[docs/CONSUMING.md](./docs/CONSUMING.md)**.
## Develop
```bash
uv sync --all-extras # install every extra ([web] included) + dev tools
./scripts/lint # ruff check + ruff format --check + mypy (config in pyproject)
./scripts/test # pytest
./scripts/build-js # both JS packages: the npm view bridge, and the web
# client, which builds into src/mcp_agent_api/ui (needs node)
```
## Releases
Versioning and `CHANGELOG.md` are managed by
[release-please](https://github.com/googleapis/release-please) from Conventional
Commits. See [CONTRIBUTING.md](./CONTRIBUTING.md) — in short, **your PR title is
the changelog entry**, and CI fails a PR whose title isn't a valid conventional
commit. The Python package and the JS bridge share one version (linked).
This server cannot be deployed
Maintenance
ActivityActive
ResponsivenessUnresponsive