Skip to main content
Glama

Senu

English | 日本語

Each Rust operation, served as a web page, a JSON API and an MCP tool.

Senu is an experimental Rust seed. A single pure function, list_projects, is shared by three transports: an HTML page rendered with Askama, a JSON API on Axum, and an opt-in MCP tool over stdio built with the official rmcp SDK. Each transport checks its own credential, picks a principal on the server side and calls the same function, so all three return the same projects in the same order. A test checks that. Senu also serves a small site of Markdown articles, which holds a beginner course.

Aims (MCS): minimal, consistent, spec-driven

  • Minimal: application developers should own as little code, configuration and duplication as possible. Today a new article is one Markdown file, and a change of look is classes in a template plus npm run css.

  • Consistent: one set of conventions for people and AI tools, kept by rustfmt, clippy -D warnings and, for the stylesheet, a CI drift gate. These check form; they do not prove that the business rules are right.

  • Spec-driven: a spec in docs/specs/ comes first, then tests, then code; the development log maps spec 0004's acceptance criteria to their tests.

Rust is the foundation for its memory and type safety and its speed, but the language alone does not make a program secure or fast. Newer tools, such as Tailwind CSS here, are adopted where they serve these aims. Done: the shared operation on three transports, the article site and the Tailwind UI (spec 0004). Planned, not built: operations declared once for all transports (spec 0005).

Related MCP server: redmine-mcp

New to Rust? Start with the course

The beginner course is a small site that Senu serves on your own computer; it is not published on the web. Install Rust and Git as its installation chapter describes, then run:

cd ~
git clone https://github.com/efg-technologies/senu.git
cd ~/senu
cargo run --quiet -- site

The first build takes several minutes. Then open http://127.0.0.1:3000/ (English) or http://127.0.0.1:3000/ja/ (Japanese). The course needs no credentials; Beginner course and senu site has the details. The rest of this README describes the transport demo.

Status: experimental local demo

Senu is a runnable seed, not a product, and it is not audited security software. It makes no performance or security claims.

  • Fixed demo data. The catalogue is five projects in two tenants, built into the binary and held in memory. Nothing is stored or persisted, and there are no mutations.

  • senu serve listens on a loopback address only. /projects uses HTTP Basic and /api/projects uses a Bearer token. There is no TLS: credentials travel in cleartext over loopback, where other local users can reach them. Basic auth here is not a production login. There are no sessions, login UI, OAuth, users or roles, rate limiting, request logging or audit trail.

  • senu mcp --enable-mcp speaks MCP over stdio and is off unless that flag is given. SENU_MCP_TOKEN must be set before it starts. Under the MCP authorization spec, stdio servers take credentials from the environment, so this trusts the local process that launches Senu. It does not authenticate any remote party. There is no MCP over HTTP and no OAuth.

  • Mutations need more work. Basic auth is an ambient browser credential, so any future write operation needs CSRF protection and a new spec.

  • There are no generators or scaffolding, and no published crate: run Senu from a clone of this repository.

Quickstart

You need the Rust toolchain pinned in rust-toolchain.toml (for example through rustup), bash, curl and python3. From a clone, run scripts/quickstart.sh. This is the whole script:

#!/usr/bin/env bash
# Senu quickstart: build, call the JSON API and the HTML page, then call the
# MCP tool over stdio and check every reply. Run it from a clone of the
# repository; it needs cargo, curl and python3. The credentials are demo values
# for a loopback-only server; choose your own for anything else.
set -euo pipefail

command -v python3 >/dev/null || { echo "quickstart: python3 is required" >&2; exit 1; }
cargo build --locked --quiet
senu=./target/debug/senu
log=$(mktemp)

SENU_BIND=127.0.0.1:0 SENU_WEB_USER=demo SENU_WEB_PASSWORD=demo-password \
  SENU_API_TOKEN=demo-api-token "$senu" serve 2>"$log" &
server=$!
trap 'kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true; rm -f "$log"' EXIT

url=
for _ in $(seq 100); do
  url=$(sed -n 's/^senu: listening on //p' "$log")
  [ -n "$url" ] && break
  sleep 0.1
done
[ -n "$url" ] || { cat "$log" >&2; exit 1; }

echo "== JSON API (Bearer): $url/api/projects?q=launch"
curl -fsS -H 'Authorization: Bearer demo-api-token' "$url/api/projects?q=launch"
echo
echo "== HTML page (Basic): $url/projects?q=launch"
curl -fsS -u demo:demo-password "$url/projects?q=launch" | grep 'data-project-id'

echo "== MCP over stdio: initialize, then tools/call list_projects {\"q\":\"launch\"}"
SENU_MCP_TOKEN=demo-mcp-token python3 scripts/check_mcp.py "$senu" mcp --enable-mcp

The MCP step runs scripts/check_mcp.py, a small client that uses only the Python standard library. It waits for the initialize reply before calling the tool, and it fails, killing the server, unless each reply carries the request's id, no JSON-RPC error and no isError, and the call returns acme-001 and acme-003. Any failed step makes the quickstart exit non-zero.

The HTML output shows Askama's escaping: the demo project named <script>alert('x')</script> & "Launch" Notes is rendered as text.

Beginner course and senu site

senu site serves a beginner course for people who know no Rust, written as Markdown articles in content/en and content/ja and served by Senu itself on a loopback address:

cargo run --quiet -- site

Then open http://127.0.0.1:3000/ (English) or http://127.0.0.1:3000/ja/ (Japanese). The course starts with a visible page in the browser, has the learner write notes, and then rebuild the reading-time function test first; two optional appendices cover the catalogue, the API and MCP.

Any file named like content/en/51-my-note.md (two digits, a lowercase slug and .md) becomes the page /my-note the next time cargo run or cargo build runs: build.rs embeds the files, so the binary reads nothing at run time and needs a rebuild after every change. The first line must be a # Title. Raw HTML is shown as text, images are refused, and links must name an existing page exactly or start with https://; a broken rule stops startup with the file, the line and the reason. The content is trusted, written by the repository's authors; this is not a system for untrusted input. senu site takes only SENU_BIND and refuses every credential variable; it has no /projects, /api/* or MCP routes. Spec 0003 describes the articles, the site and the course; it supersedes the mdBook tutorial of spec 0002. The site is not published anywhere; CI keeps a static snapshot as an artifact.

Commands

Command

What it does

senu serve

HTML and JSON on SENU_BIND (default 127.0.0.1:3000, loopback IP literals only, port 0 allowed)

senu site

the Markdown articles as HTML on SENU_BIND, with no credentials

senu mcp --enable-mcp

MCP over stdio until stdin closes; exits 0 on EOF

senu --help

usage

serve needs SENU_WEB_USER (1–64 visible ASCII characters, no :), SENU_WEB_PASSWORD and SENU_API_TOKEN (1–256 visible ASCII characters). mcp needs SENU_MCP_TOKEN. All credentials must differ, and unknown SENU_* variables are rejected. A configuration error exits 2 and names the variable without printing its value; a bind failure exits 1. Tenants and roles cannot be configured.

Routes and the MCP tools

  • GET /projects?q= (Basic): an HTML list with a search form.

  • GET /api/projects?q= (Bearer): JSON {"projects": [{"id", "name"}]}.

  • GET /project?id= (Basic): one project's HTML page.

  • GET /api/project?id= (Bearer): JSON {"id", "name"}.

  • GET /_senu/ui.css (no credentials, also under senu site): the stylesheet, text/css, served from memory. /_senu/ is reserved for Senu; every other path below it is 404.

  • MCP tool list_projects: readOnlyHint: true, optional string q, no other fields. Results come back as structuredContent and as JSON text; operation errors are isError results with {"error": code}. An unknown tool gets JSON-RPC error -32602 and a method that rmcp does not recognise gets -32601; both carry fixed messages that do not repeat the requested name.

  • MCP tool get_project: readOnlyHint: true, required string id, no other fields, with the same results and errors.

id is required: 1 to 32 characters of a-z, 0-9 and -, matched exactly; any other value is invalid_id (400). An id that is unknown or belongs to another tenant is not_found (404) on every transport. q is trimmed, then matched case-insensitively against project names. After trimming it may hold up to 80 Unicode scalar values and no control characters. Unknown fields are rejected. Every other path returns 404 and every other method 405. Request bodies (413) and targets over 2048 bytes (414) are refused, and credentials in the query string are never read. Responses carry Cache-Control: no-store and X-Content-Type-Options: nosniff, and errors never repeat input or secrets.

Templates

Templates are Askama .html files with HTML escaping on. askama.toml configures an ERB-like syntax:

<%# comments %>
<%! for p in projects %>
<li data-project-id="<%= p.id %>"><%= p.name %></li>
<%! endfor %>

Blocks use <%! %>, expressions <%= %> and comments <%# %>. Plain <% for blocks does not work, because Askama rejects a delimiter that is a prefix of another. A test fails if a template or the configuration uses the safe filter or turns escaping off. The one exception is the article body, which is TrustedHtml from the Markdown policy: the same test requires exactly one HtmlSafe impl in src/, on that type.

Look: Tailwind CSS and Typography

The pages use Tailwind CSS utility classes in templates/*.html and the official Typography plugin's prose styles for article text. Change the look by editing classes in a template, or theme variables for every page in assets/ui.css, the only CSS source:

@theme {
  --font-sans: "Trebuchet MS", "Noto Sans JP", sans-serif;
}

Tailwind writes rules only for the classes it finds in the templates, into assets/ui.generated.css. That file is committed and built into the binary, so Cargo alone builds and runs Senu; Node is needed only to change the look. With Node 24 and its npm:

npm ci            # once: installs the exact versions in package-lock.json
npm run css       # after changing classes or the theme
cargo run -- site # or serve, as before

Commit the source together with assets/ui.generated.css. CI runs npm ci, npm run css and git diff --exit-code -- assets/ui.generated.css, and cargo test fails when a template class has no rule. Typography keeps its own colours, so @theme does not change link colours inside articles; a prose-a: class on <main> in templates/site_base.html does. The licenses of both packages are in THIRD-PARTY-NOTICES.txt, and the stylesheet keeps their notices.

Development

cargo fmt --check
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked -- --test-threads=2
scripts/quickstart.sh
python3 scripts/check_tutorial.py
python3 scripts/snapshot_site.py target/site

The tests include tests/quickstart_check.rs, which runs the quickstart's MCP checker against fake servers, and tests/tutorial_checker.rs, which runs the own tests of the tutorial checker and of the site snapshot; both need python3. check_tutorial.py runs the course in English and then in Japanese, each in a temporary copy of the last commit, and leaves the working tree unchanged; commit your changes first, because it refuses to run while tracked files differ from HEAD. The course's optional last section runs npm ci, so check_tutorial.py also needs Node 24, npm and access to the npm registry. snapshot_site.py needs a new or empty directory; it starts senu site, saves every page as a static file with relative links, the stylesheet and the notices, and checks them. CI runs the same commands on GitHub-hosted Ubuntu and uploads the snapshot as an artifact; nothing is deployed. Read the specs, the development log and the research notes for the details.

License

Senu is licensed under Apache-2.0.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides project management capabilities through terminal, web UI, or MCP stdio, including plans, code analysis, and filesystem-safe operations.
    6 npm
    MIT
  • F
    license
    A
    quality
    B
    maintenance
    Provides read-only MCP tools over stdio for interacting with a Redmine instance, enabling users to list and retrieve projects, issues, queries, wiki pages, and other Redmine resources via the REST API.
    35
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables MCP clients to query projects, fetch findings, trigger analysis, upload CycloneDX BOMs, and check async token status against an OWASP Dependency-Track instance via stdio.
    20 npm
    MIT
  • A
    license
    B
    quality
    A
    maintenance
    Enables MCP clients to manage a project-local tracker by exposing sprint, task, and subtask operations as tools over stdio.
    19
    117 PyPI
    MIT