Skip to main content
Glama

fitnesse-mcp

License: MIT FastMCP

An MCP server that exposes FitNesse's REST responders as tools, letting an MCP client read wiki pages, run tests and suites, manage the files section, and inspect test history on a FitNesse instance.

Copyright (c) 2026 netcare GmbH. Released under the MIT License.


Requirements

Working outside the devcontainer? You'll need Python 3.12+ and FastMCP 4, which is currently a prerelease and must be pinned exactly:

pip install "fastmcp==4.0.0b2"

Using uv? fastmcp is a thin wrapper that depends on fastmcp-slim at the same version, and uv only allows prereleases for packages you name explicitly:

[project]
dependencies = ["fastmcp==4.0.0b2"]

[tool.uv]
constraint-dependencies = ["fastmcp-slim==4.0.0b2"]

Pin exactly, not >=4.0.0b1. Each beta in the v4 line has carried breaking changes. This project tracks the prerelease and the pin will move at GA.


Related MCP server: JSON Path MCP Server

Quickstart

1. Open the project in its devcontainer.

git clone https://github.com/netcare-io/fitnesse-mcp.git
cd fitnesse-mcp
code .

VS Code detects .devcontainer/ and prompts to reopen in the container — accept it, or run Dev Containers: Reopen in Container from the command palette (F1). The first build takes a few minutes; later starts are quick. Python and all dependencies are installed inside the container, so there's nothing to set up on your host.

2. Point the server at your FitNesse instance.

Run the remaining commands in the container's terminal:

export FITNESSE_BASE_URL=http://your-fitnesse-host:8080
export FITNESSE_READONLY=1          # recommended for a first run

fastmcp run server.py

localhost in that URL refers to the container, not your host — see Troubleshooting if the connection is refused.


Security

This server gives an LLM client the ability to delete pages, purge test history, and roll back versions on your FitNesse instance. Two controls limit that, and both are opt-in:

  • Start with FITNESSE_READONLY=1. This hides every write, execute, and control tool, leaving only the read-only tools (see Tools for counts). Open it up deliberately once you know which operations you actually want the model to perform.

  • fitnesse_shutdown is not registered at all unless FITNESSE_ALLOW_SHUTDOWN is set. It stops the FitNesse server.

Three further notes:

  • fitnesse_list_files, fitnesse_create_dir, fitnesse_upload_file, fitnesse_download_file, fitnesse_delete_file, and fitnesse_rename_file are not registered at all unless FITNESSE_FILES_ROOT is set.

  • Every files tool is confined to the resource root named by FITNESSE_FILES_ROOT on the remote FitNesse instance (default files, but some instances configure a different root): files_path must start with that root and may not contain .., and filename / dirname / new_name must be plain names without path separators. A call aiming outside it (e.g. files_path: "FrontPage") is rejected before any request goes out, so these tools cannot reach wiki pages or other responders. Neither fitnesse_upload_file nor fitnesse_download_file touches local disk: content travels through the tool call itself, so both work even when the MCP server and the calling client don't share a filesystem. fitnesse_download_file returns the file's content directly in its result — as an inline image for image/* content, or as a text/binary resource otherwise. fitnesse_upload_file takes the content as an argument — plain text via content, or base64-encoded bytes via content_base64 — instead of a local file path.

  • Credentials in claude_desktop_config.json are stored in cleartext. For shared machines, prefer the HTTP pattern below with the credentials in the server's own environment.


Environment variables

Variable

Default

Description

FITNESSE_BASE_URL

http://localhost:8080

FitNesse server base URL

FITNESSE_USERNAME

(none)

Basic Auth username

FITNESSE_PASSWORD

(none)

Basic Auth password

FITNESSE_READONLY

false

1/true/yes/on hides all write, execute, and control tools

FITNESSE_ALLOW_SHUTDOWN

false

1/true/yes/on exposes fitnesse_shutdown

FITNESSE_COMPLETE_TOOLSET

false

1/true/yes/on exposes the 14 lower-traffic tools hidden by default (marked below)

FITNESSE_FILES_ROOT

(none — files tools disabled)

Resource root of the files section on the FitNesse instance (e.g. files); enables fitnesse_list_files, fitnesse_create_dir, fitnesse_upload_file, fitnesse_download_file, fitnesse_delete_file, and fitnesse_rename_file

FITNESSE_MAX_RESPONSE_BYTES

1048576 (1 MB)

Responses above this are truncated and flagged "truncated": true

FITNESSE_FILES_MAX_TRANSFER_BYTES

10485760 (10 MB)

Uploads above this are rejected; downloads above this are truncated and rejected


Tools

23 tools by default, each mapping to one FitNesse responder. Set FITNESSE_FILES_ROOT to the files-section resource root on your FitNesse instance to also expose fitnesse_list_files, fitnesse_create_dir, fitnesse_upload_file, fitnesse_download_file, fitnesse_delete_file, and fitnesse_rename_file (29 total), FITNESSE_COMPLETE_TOOLSET=1 to expose 14 additional lower-traffic tools, and FITNESSE_ALLOW_SHUTDOWN=1 to also expose fitnesse_shutdown (44 with all four). Under FITNESSE_READONLY, only the read tools are exposed — 9 by default, or up to 24 with FITNESSE_FILES_ROOT and FITNESSE_COMPLETE_TOOLSET=1 both set.

Pages — read

Tool

Responder

fitnesse_get_page

getPage (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_get_raw

raw (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_get_page_data

pageData

fitnesse_get_packet

packet — all tables on a page, as JSON (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_get_properties

properties

fitnesse_get_variables

variables

fitnesse_list_names

names

fitnesse_edit_page

edit — with redirect/nonExistent options (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_get_page_content

edit — convenience alias (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_get_new_page_form

new (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_get_refactor_screen

refactor (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_get_rss

rss

Pages — write

Tool

Responder

fitnesse_add_child_page

addChild

fitnesse_save_page_content

saveData (POST form data)

fitnesse_save_properties

saveProperties (POST form data)

fitnesse_rename_page

renamePage

fitnesse_move_page

movePage

fitnesse_delete_page

deletePage

fitnesse_manage_symlink

symlink

fitnesse_import_pages

import

fitnesse_import_and_view

importAndView

fitnesse_publish

publish (needs FITNESSE_COMPLETE_TOOLSET)

Tests

Tool

Responder

Mode

fitnesse_run_test

test

execute

fitnesse_run_suite

suite — suite filters, debug, nochunk

execute

fitnesse_get_instruction

instruction — Slim instructions (needs FITNESSE_COMPLETE_TOOLSET)

read

fitnesse_stop_test

stoptest

control

fitnesse_shutdown

shutdown (needs FITNESSE_ALLOW_SHUTDOWN)

control

History & versions

Tool

Responder

Mode

fitnesse_get_test_history

testHistory (needs FITNESSE_COMPLETE_TOOLSET)

read

fitnesse_get_page_history

pageHistory

read

fitnesse_compare_history

compareHistory

read

fitnesse_get_versions

versions

read

fitnesse_view_version

viewVersion

read

fitnesse_rollback_version

rollback

write

fitnesse_purge_history

purgeHistory

write

Tool

Responder

fitnesse_search

search — read (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_execute_search_properties

executeSearchProperties — read (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_get_search_form

searchForm — read (needs FITNESSE_COMPLETE_TOOLSET)

fitnesse_where_used

whereUsed — read (needs FITNESSE_COMPLETE_TOOLSET)

Files section

Tool

Responder

Mode

fitnesse_list_files

files (needs FITNESSE_FILES_ROOT)

read

fitnesse_download_file

(direct file GET, returned inline as image/text/binary) (needs FITNESSE_FILES_ROOT)

read

fitnesse_create_dir

createDir (needs FITNESSE_FILES_ROOT)

write

fitnesse_upload_file

upload (POST multipart, content passed inline) (needs FITNESSE_FILES_ROOT)

write

fitnesse_rename_file

renameFile (needs FITNESSE_FILES_ROOT)

write

fitnesse_delete_file

deleteFile (needs FITNESSE_FILES_ROOT)

write

Flag-style FitNesse inputs are supported by passing None as a query param value — for example {"nohistory": None} produces ?nohistory.


Connecting an MCP client

Pattern 1 — stdio (simple, local)

The client launches the server as a subprocess and talks over stdin/stdout. No server process to manage.

{
  "mcpServers": {
    "fitnesse": {
      "command": "fastmcp",
      "args": ["run", "server.py"],
      "env": {
        "FITNESSE_BASE_URL": "http://your-fitnesse-host:8080",
        "FITNESSE_USERNAME": "your-username",
        "FITNESSE_PASSWORD": "your-password",
        "FITNESSE_READONLY": "1"
      }
    }
  }
}

Running the server from a Docker image instead? -i keeps stdin open, and each -e forwards one variable from the env block into the container. Every variable you set in env needs its own -e flag — anything missing here is silently ignored inside the container:

{
  "mcpServers": {
    "fitnesse": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "-e", "FITNESSE_BASE_URL",
        "-e", "FITNESSE_USERNAME",
        "-e", "FITNESSE_PASSWORD",
        "-e", "FITNESSE_READONLY",
        "fitnesse-mcp:latest"
      ],
      "env": {
        "FITNESSE_BASE_URL": "http://your-fitnesse-host:8080",
        "FITNESSE_USERNAME": "your-username",
        "FITNESSE_PASSWORD": "your-password",
        "FITNESSE_READONLY": "1"
      }
    }
  }
}

Pattern 2 — HTTP (shared, production)

Use this when several clients share one server instance, or when the server runs in Docker. Credentials live in the server's environment rather than in each client's config.

FITNESSE_BASE_URL=http://your-fitnesse-host:8080 \
FITNESSE_USERNAME=your-username \
FITNESSE_PASSWORD=your-password \
fastmcp run server.py --transport http

Listens on port 8000 by default; override with --port.

Most clients can point at the URL directly:

{
  "mcpServers": {
    "fitnesse": { "url": "http://localhost:8000/mcp" }
  }
}

For clients that only speak stdio, fastmcp run <url> proxies to it (internally).

Pattern 3 — devcontainer via docker exec

If you already keep the project's devcontainer running (it's named fitnesse-mcp-devcontainer, see .devcontainer/devcontainer.json), an MCP client can attach to it directly instead of spinning up a separate image:

{
  "mcpServers": {
    "fitnesse": {
      "command": "docker",
      "args": ["exec", "-i", "fitnesse-mcp-devcontainer", "fastmcp", "run", "server.py"]
    }
  }
}

FITNESSE_* variables aren't passed on this command line — docker exec inherits whatever environment is already baked into the container. devcontainer.json's containerEnv block sets them from your host environment (${localEnv:FITNESSE_BASE_URL} etc.) when the container is created; export the vars on your host and rebuild the devcontainer for changes to take effect, or edit the defaults in containerEnv directly.

If you'd rather proxy to an already-running HTTP server on port 8000 inside the container (Pattern 2's stdio proxy), swap the last three args for run http://127.0.0.1:8000/mcp — but note nothing starts that server automatically; you'd still need to run fastmcp run server.py --transport http inside the container yourself first.


Interactive testing

Create fastmcp.json file.

./scripts/run-inspector.sh

Open the exact URL printed in the terminal — it carries a ?MCP_INSPECTOR_API_TOKEN=... query parameter.


Troubleshooting

401 Unauthorized — either FITNESSE_USERNAME/FITNESSE_PASSWORD are wrong, or FitNesse isn't configured for authentication and is rejecting the header. Confirm with curl -u user:pass "$FITNESSE_BASE_URL/FrontPage?responder=raw".

Connection refused — check FITNESSE_BASE_URL. Inside a container, localhost is the container, not your host; use host.docker.internal (Docker Desktop) or the host's LAN address.

A write tool is missingFITNESSE_READONLY is set. Note that any value other than 1/true/yes/on counts as unset.

"truncated": true in a response — the body exceeded FITNESSE_MAX_RESPONSE_BYTES and was cut. Common on suite runs with includehtml. Raise the limit or narrow the request.

Invalid path — the page path contained ?, #, .., or a null byte. FitNesse page paths are dotted (FrontPage.MySuite.MyTest) with no leading slash.

ImportError on startup — almost certainly the FastMCP version. This project targets 4.0.0b2 exactly; v3 and the v4 alphas will not import.


Development

pip install -e ".[dev]"
pytest tests/

The test suite runs the server in-process via fastmcp.Client, so it also verifies the pieces that only fail at call time: dependency injection of timeouts, tag-based tool visibility, and path-injection rejection. Run it after any FastMCP version bump — it doubles as the upgrade tripwire.

Devcontainer

The devcontainer (see Quickstart) builds from a multi-stage Dockerfile at .devcontainer/Dockerfile:

  • devcontainer — used by VS Code for development

  • test — installs dev dependencies; used by scripts/release.sh to run the test suite without requiring Python on the host

  • production — used by Docker Compose for deployment

Release

Releasing is split into two scripts so cutting a release doesn't require Docker registry access, and publishing the image doesn't require pushing to git. scripts/release-and-publish.sh runs both in sequence.

scripts/release.sh — tests, version bump, git tag

Prerequisites: clean working tree, checked out on main; push access to origin; docker (to run the test suite — no local Python needed); gh auth login, if you want the GitHub release created automatically.

bash scripts/release.sh 0.2.0

This runs, in order:

  1. Builds the test Docker target and runs pytest inside it — aborts the release on failure

  2. Bumps version in pyproject.toml to 0.2.0

  3. Commits the bump and tags it v0.2.0

  4. Pushes the commit and the v0.2.0 tag to origin

  5. Opens a GitHub release for v0.2.0 via gh (prints the manual-create link instead if gh isn't installed)

scripts/docker-build-and-publish.sh — build + push the image

Prerequisites: docker login harbor.netcare.local (or export FITNESSE_MCP_REGISTRY to target a different registry).

bash scripts/docker-build-and-publish.sh 0.2.0

Builds the production Docker target tagged 0.2.0 and latest, then pushes both to harbor.netcare.local/fitnesse-mcp. Run it from the tagged commit (e.g. right after release.sh, or after git checkout v0.2.0 later); if the version is omitted it's read from pyproject.toml.

scripts/release-and-publish.sh — both, in one step

bash scripts/release-and-publish.sh 0.2.0

Production with Docker Compose

docker compose up --build -d

Env vars come from a .env file alongside docker-compose.yml:

FITNESSE_BASE_URL=http://your-fitnesse-host:8080
FITNESSE_USERNAME=your-username
FITNESSE_PASSWORD=your-password
FITNESSE_READONLY=1
# FITNESSE_ALLOW_SHUTDOWN=1
# FITNESSE_COMPLETE_TOOLSET=1
# FITNESSE_FILES_ROOT=files

The server is then available at http://localhost:8000/mcp or through the stdio-to-html-proxy for stdio-only clients:

{
  "mcpServers": {
    "fitnesse": {
      "command": "docker",
      "args": [
        "exec", "-i", "fitnesse-mcp",
        "fastmcp", "run", "http://127.0.0.1:8000/mcp"
      ]
    }
  }
}

Related MCP Connectors

Related MCP Servers