10. Model Context Protocol

Model Context Protocol, usually abbreviated MCP, is a simple way for an application to expose tools and structured data to an LLM-based client. Instead of asking the model to inspect files directly, the client talks to a server that offers named tools such as discovery, lookup, and read-only data access.

From the user’s point of view, MCP is mostly an interoperability layer: the client knows which tools exist, how to call them, and how to interpret the results. The server keeps the actual data and behavior behind a small, documented interface.

For Waterloo, that interface is used to expose JSON-based documentation data and a small set of helper tools that make large documentation trees easier to navigate from an LLM agent or a visual inspector.

10.1. The Waterloo MCP-Server

The Waterloo MCP-server applies that pattern to Waterloo documents in JSON format. It gives LLM-agents a simple, standardized way to access and query the documentation without loading the whole data set into the context window. The server is implemented as a simple HTTP server that listens for JSON-RPC requests and responds with JSON data. It can be integrated with LLM-agents that support JSON-RPC.

10.2. Executable and configuration

The Waterloo package includes a ready-to-use configuration file for the MCP server at etc/wtrl_mcp.http.toml. Once the package is installed, the server can be started in a terminal with the following command:

wtrl_mcp --config etc/wtrl_mcp.http.toml

If the argument to --config is an absolute path, it is used as is. If it is a relative path, wtrl_mcp first looks for the file relative to the current working directory and then relative to the installed Waterloo package root. The usual package-local configuration path is therefore prefixed with etc/. If neither candidate exists, the server reports a clear configuration error and exits.

10.3. Tools

The MCP-server supports the following tools:

Tool discovery

  • describe_tool — reads the Waterloo signature and docstring for one MCP tool.

Root discovery and inventory

  • list_roots — lists the configured Waterloo data roots.

  • get_root — reads one configured Waterloo data root by root_id.

  • get_root_metadata — reads only the compact header metadata of one configured Waterloo root by root_id.

  • list_objects — lists all Waterloo objects in one configured root.

Object content access

  • get_object — reads one Waterloo object by qid from a configured root.

  • get_section — reads one stored section of one Waterloo object.

  • get_subsection — reads one stored subsection of one Waterloo object.

  • get_signature — reads the stored signature block for one Waterloo object.

Reference and graph lookup

  • get_references — reads structured incoming See_also references for one Waterloo object.

  • search_related — reads the star-shaped See_also neighborhood for one Waterloo object.

Example lookup

  • get_examples — reads structured example metadata for one Waterloo object.

  • get_example_source — reads the source text for one canonical Waterloo example reference.

Search tools

  • search_objects — searches Waterloo objects by expression and structural filters.

  • search_sections — searches Waterloo section and subsection labels by expression and structural filters.

  • search_text — searches Waterloo text content by terms and structural filters.

Authoring helper

  • gen_docstring — generates a Waterloo docstring template for a given profile, with optional signature, template mode, and indentation mode.

10.4. Typical agent workflow

The tools above are the catalog; the workflow below is a recommended way to approach them when the agent does not already know the exact tool or target. In practice there are two common starting points:

  • discover the MCP tool set itself with describe_tool

  • discover Waterloo content with list_roots

The lookup-oriented tools are then meant to be used in a simple progression. This is only a recommended progression; agents that already know the target can jump directly to the relevant get_* or search_* tool.

  1. list_roots to discover available roots.

  2. describe_tool when the agent wants to inspect the signature and Waterloo docstring of one MCP tool.

  3. list_objects to inventory the objects in one root before drilling down.

  4. search_objects to find candidate objects and obtain stable root_id / qid pairs.

  5. get_section or get_subsection to inspect the relevant contract, notes, or other structured sections.

  6. search_sections when the agent wants to find section or subsection labels rather than object names.

  7. get_signature when the agent wants to inspect the canonical stored signature for one object.

  8. get_references when the agent wants to inspect incoming structured See_also references for one object.

  9. search_related when the agent wants a compact star-shaped See_also neighborhood around one object.

  10. get_examples when the agent wants to inspect available example references for one object.

  11. get_example_source when the agent wants to retrieve the raw source text for one canonical example reference.

  12. search_text when the agent wants to search the actual content text with a small set of terms.

  13. gen_docstring when the agent wants to draft or refine a Waterloo docstring template for a profile.

In practice this means that the agent can start with a vague name, narrow it down to a canonical target, and then inspect the exact Waterloo text that describes the object.

10.5. Using the MCP-server in VSCode

HTTP transport

[Last tested with VSCode 1.115.0 on 2026-05-31]

With transport streamable-http, the MCP-server is added to VSCode by:

Shift+Ctrl+P and MCP: Open User Configuration

The code to be added should look like

{
    "servers": {
        "waterloo-docs": {
                "type": "http",
                "url": "http://127.0.0.1:13316/mcp"
        }
    }
}

where the url is the URL of the MCP server. Use the host and port from the running server configuration, and make sure the URL matches the configured streamable-http endpoint. The MCP server must be running and accessible at that URL for the integration to work. As a smoke test, open the MCP panel in VSCode and see whether it connects successfully. Then ask Copilot to run list_roots and check whether it returns the expected list of documents.

Stdio transport

With transport stdio, the setup is usually more convenient for day-to-day use because no separate terminal and no TCP port are needed. If the Waterloo extension and the Python package sdv.doc.waterloo are installed, VSCode can talk to the MCP-server directly through the stdio transport. The extension contributes the server automatically, so the user does not need to add a JSON server definition by hand.

The default configuration lives in etc/wtrl_mcp.stdio.toml. If you want to customize the roots or other settings, make a copy of that file and point VSCode to the copy. Open the command palette with Shift+Ctrl+P, choose MCP: Open User Configuration, and then set waterloo.mcpConfigPath to your copied stdio configuration. The path may be absolute (recommended) or relative; if it is relative, the extension also looks in the open workspace and in the installed Waterloo package root.

If you want to disable the automatic MCP server contribution entirely, set waterloo.mcpProvideServer to false. Otherwise the extension will use the configured command and configuration path, with sensible defaults when no custom path is given.

After saving the settings, open the MCP panel in VSCode and check whether the server appears. As a smoke test, ask Copilot to run list_roots and verify that the expected roots are returned.

10.6. Server error messages

This section is normative.

The MCP implementation currently returns textual tool errors through the FastMCP transport path. The Waterloo-specific error payload classes are kept as a draft in mcp/wtrl_error.py, but the wire-level structure is not normative yet.

The current lookup-oriented rule family is:

  • [MCPS-001] – unknown or missing root_id.

  • [MCPS-002] – unknown qid.

  • [MCPS-003] – unknown section name.

  • [MCPS-004] – unknown subsection name.

  • [MCPS-005] – root document too large for the get_root guardrail.

  • [MCPS-006] – unknown example reference.

  • [MCPS-007] – unknown tool name.

The current implementation already prefixes tool error messages with these rule labels. A later version may map them more directly to structured JSON-RPC error payloads if the MCP SDK exposes a better hook for that.

10.7. Running the MCP Server in a Docker Container

To provide an MCP server in a platform-independent way, waterlint can generate a Dockerfile and, depending on the selected mode, one or two helper scripts from an MCP server configuration.

The generated container provides a complete deployment unit for a Waterloo MCP server, including configuration and, optionally, the referenced root documents.

The corresponding subcommand is render-docker. The basic invocation syntax is:

waterlint render-docker --in /path/to/mcp.toml --out /path/to/dockerfile

Two operating modes are available:

  • --no-bake-roots – Root documents are mounted into the container at runtime.

  • --bake-roots (default) – Root documents are embedded into the Docker image.

Additional options are described in the waterlint online help. In particular, render-docker accepts --public-port to describe the externally published container port and --allowed-hosts to name the hosts that should be allowed for that port in the generated configuration.

During development, or while actively extending a project’s documentation, the --no-bake-roots mode is typically preferred. This avoids rebuilding the Docker image whenever documentation changes; restarting the container is sufficient.

Starting such a container manually would be inconvenient because the required document roots must be mounted explicitly. Therefore, waterlint render-docker additionally generates a launch script for this mode. The script starts the container in the foreground and forwards logging output to stdout.

For deployment, the --bake-roots mode is usually preferred, because only a single artifact – the Docker image itself – needs to be distributed.

After generating the Dockerfile and scripts, waterlint render-docker prints a number of example Docker commands, including commands for running the container as a daemon.

The generated Docker image acts as a deployable Waterloo documentation service. The overall workflow is illustrated in the following diagram:

Workflow for waterlint's Docker output

The package installation directory relevant for the following examples can be obtained using:

python3 -c "import importlib.resources as r; print(r.files('sdv.doc.waterloo'))"

We refer to this path as ${ROOT}.

Among other files, directory ${ROOT}/etc contains an MCP server configuration and a logging configuration referenced by that server configuration:

  • wtrl_mcp.http.toml

  • logging.toml

For example, the MCP server can be started with HTTP transport using:

wtrl_mcp --config ${ROOT}/etc/wtrl_mcp.http.toml

This configuration defines an MCP server using transport streamable-http, listening on port 13316. The generated Docker image automatically exposes this port.

10.7.1. Bake Mode (--bake-roots, default)

We use the above configuration as an example for running the MCP server inside a Docker container:

waterlint render-docker --in ${ROOT}/etc/wtrl_mcp.http.toml --out /tmp/my_wtrl_mcp

This generates Dockerfile /tmp/my_wtrl_mcp and build script /tmp/build.my_wtrl_mcp.sh. The build script uses --no-cache by default and accepts --cache when you want to reuse Docker layers explicitly. If the image will be published on a different port than the server listens on inside the container, add for example --public-port 13315 --allowed-hosts localhost 127.0.0.1.

The Docker image is then built simply by executing:

/tmp/build.my_wtrl_mcp.sh

Afterwards, Docker should list the image:

docker image ls | grep my_wtrl_mcp
wtrl-mcp-my_wtrl_mcp    latest    bcd1f03f9682    34 seconds ago   274MB

The image can now be started in the foreground for testing. It is important to forward the configured port number (13316 in this example):

docker run --rm -p 13316:13316 wtrl-mcp-my_wtrl_mcp

The MCP server should report messages similar to the following (line wrapping added for readability):

2026-06-06T06:29:03 INFO: WTRL Ready to serve via Uvicorn.
2026-06-06T06:29:03 INFO: WTRL Loaded 1 configured roots.
2026-06-06T06:29:03 INFO: WTRL Configured root:
                          /shared/doc/wtrl_mcp.wtrl.core.rfc-2119.eb1497962ecf.json ...
2026-06-06T06:29:03 INFO: WTRL Built reference index with 13 target entries
                          and 39 source references.
2026-06-06T06:29:03 INFO: Started server process [1] [_serve]
2026-06-06T06:29:03 INFO: Waiting for application startup. [startup]
2026-06-06T06:29:03 INFO: StreamableHTTP session manager started [run]
2026-06-06T06:29:03 INFO: Application startup complete. [startup]
2026-06-06T06:29:03 INFO: Uvicorn running on http://0.0.0.0:13316
                          (Press CTRL+C to quit) [_log_started_message]

Alternatively, the container can be run as a daemon:

docker run -d --name wtrl-mcp-my_wtrl_mcp -p 13316:13316 wtrl-mcp-my_wtrl_mcp

and stopped in the usual Docker way:

docker stop wtrl-mcp-my_wtrl_mcp

10.7.2. No-Bake Mode (--no-bake-roots)

If the MCP server roots should not be embedded into the image, generate the Dockerfile as follows:

waterlint render-docker --in ${ROOT}/etc/wtrl_mcp.http.toml --out /tmp/my_wtrl_mcp --no-bake-roots

Unlike Bake Mode, an additional launch script /tmp/launch.my_wtrl_mcp.sh is generated.

This script is required because manually starting the container becomes impractical once the MCP server roots are supplied externally.

The image is built and started using:

/tmp/build.my_wtrl_mcp.sh
/tmp/launch.my_wtrl_mcp.sh

Logging is written to stdout.

The container runs in the foreground and can be stopped with CTRL C.