Skip to main content
Glama
mark-brannan

colregs-mcp

by mark-brannan

colregs-mcp

Preview, 0.0.x. Not for navigation. The tool surface will change without notice.

An MCP server that answers "what lights may this vessel show" from colregs-engine, and hands back the answer in a form a language model cannot quietly simplify: every lawful display, every light with its own modality, every entry with its paragraph cite, and the verbatim rule text those cites resolve to.

It has no semantics of its own. The engine decides; this package only decides how the answer is written down. That is the whole job, and the response shape section is the part worth reading.

Try it

Not on npm yet. Clone and build:

git clone https://github.com/mark-brannan/colregs-mcp && cd colregs-mcp && npm install && npm run build

Then register the stdio server with your client. Claude Code:

claude mcp add colregs -- node /absolute/path/to/colregs-mcp/dist/cli.js

Any other MCP client: command node, argument dist/cli.js, no environment.

Tools

tool

input

returns

evaluate_display

facts

every lawful display, cited and with modality per light

applied_entries

facts

the applicability entries that hold, as {id, cite}

rule_text

cite

verbatim paragraph text from colregs data/rules.json

light

id

a light's definition from colregs data/lights.json

facts is a colregs fact record: the keys and values of colregs' data/facts.json, namespaced (fact:propulsion, propulsion:sail). The input schema is generated from that file at startup, so it follows the data. A key or value outside the vocabulary is an error, never an empty answer.

Every tool description carries the coverage limits: COLREGS Part C lights only, international text only, lights at night only, pre-release data.

Two worked examples

The same two vessels as colregs-engine's README.

A 12 m sloop, under way: three displays

{ "facts": { "fact:propulsion": "propulsion:sail", "fact:activity": "activity:none",
             "fact:position": "position:underway", "fact:length_m": 11.6 } }

Trimmed response:

{
  "colregs": { "version": "0.2.0", "source": "resolved" },
  "applied": [
    { "id": "25a", "cite": "25(a)", "modality": "shall" },
    { "id": "25b", "cite": "25(b)", "modality": "may" },
    { "id": "25c", "cite": "25(c)", "modality": "may" }
  ],
  "exempted": [], "excluded": [],
  "lawful_displays": {
    "count": 3,
    "relation": "any_one_of",
    "options": [
      { "option": 1, "of": 3, "chosen": [],
        "lights": [
          { "light": "light:sidelights", "name": "sidelights", "modality": "shall", "prescribed_by": { "id": "25a", "cite": "25(a)" } },
          { "light": "light:sternlight", "name": "sternlight", "modality": "shall", "prescribed_by": { "id": "25a", "cite": "25(a)" } } ] },
      { "option": 2, "of": 3, "chosen": [ { "id": "25b", "cite": "25(b)", "modality": "may" } ],
        "lights": [
          { "light": "light:sidelights", "name": "sidelights", "combined": true, "position": "at or near the top of the mast", "modality": "may", "prescribed_by": { "id": "25b", "cite": "25(b)" } },
          { "light": "light:sternlight", "name": "sternlight", "combined": true, "position": "at or near the top of the mast", "modality": "may", "prescribed_by": { "id": "25b", "cite": "25(b)" } } ] },
      { "option": 3, "of": 3, "chosen": [ { "id": "25c", "cite": "25(c)", "modality": "may" } ],
        "lights": [
          { "light": "light:sidelights", "name": "sidelights", "modality": "shall", "prescribed_by": { "id": "25a", "cite": "25(a)" } },
          { "light": "light:sternlight", "name": "sternlight", "modality": "shall", "prescribed_by": { "id": "25a", "cite": "25(a)" } },
          { "light": "light:all_round", "name": "all-round light", "color": "red", "count": 1, "position": "upper, at or near the top of the mast", "modality": "may", "prescribed_by": { "id": "25c", "cite": "25(c)" } },
          { "light": "light:all_round", "name": "all-round light", "color": "green", "count": 1, "position": "lower, at or near the top of the mast", "modality": "may", "prescribed_by": { "id": "25c", "cite": "25(c)" } } ] }
    ]
  },
  "optional_additions": { "relation": "any_subset_of", "items": [] },
  "modality_key": { "shall": "mandatory", "may": "permitted alternative" },
  "cited_paragraphs": {
    "25(a)": "A sailing vessel underway shall exhibit: (i) sidelights; (ii) a sternlight.",
    "25(b)": "In a sailing vessel of less than 20 meters in length the lights prescribed in Rule 25(a) may be combined in one lantern carried at or near the top of the mast where it can best be seen.",
    "25(c)": "A sailing vessel underway may, in addition to the lights prescribed in Rule 25(a), exhibit at or near the top of the mast, where they can best be seen, two all-round lights in a vertical line, the upper being red and the lower green, but these lights shall not be exhibited in conjunction with the combined lantern permitted by Rule 25(b)."
  }
}

Three is the right number, and the response is built so that "a 12 m sloop shows sidelights and a sternlight" cannot be read off it. The fact record describes a situation, not a fitted vessel. There is no equipment fact in colregs' data/facts.json (checked: nothing in that file names a lantern, a fitting or a piece of gear), so nothing in the input can settle 25(b) against 25(c). The Rules leave that to the skipper, and so does this.

A fishing vessel aground: a veto, with its source

{ "facts": { "fact:propulsion": "propulsion:power", "fact:activity": "activity:fishing",
             "fact:position": "position:aground", "fact:length_m": 30 } }
{
  "applied": [
    { "id": "26c-id", "cite": "26(c)(i)", "modality": "shall" },
    { "id": "30d-anchor", "cite": "30(d)", "modality": "shall" },
    { "id": "30d-red", "cite": "30(d)", "modality": "shall-if-practicable" }
  ],
  "excluded": [
    { "id": "30a", "cite": "30(a)", "by": { "id": "26c-id", "cite": "26(c)(i)" } },
    { "id": "30b", "cite": "30(b)", "by": { "id": "26c-id", "cite": "26(c)(i)" } }
  ],
  "lawful_displays": {
    "count": 1, "relation": "exactly_one",
    "options": [ { "option": 1, "of": 1, "chosen": [], "lights": [
      { "light": "light:all_round", "color": "red",   "count": 1, "position": "upper", "modality": "shall", "prescribed_by": { "id": "26c-id", "cite": "26(c)(i)" } },
      { "light": "light:all_round", "color": "white", "count": 1, "position": "lower", "modality": "shall", "prescribed_by": { "id": "26c-id", "cite": "26(c)(i)" } },
      { "light": "light:all_round", "color": "red",   "count": 2, "arrangement": "vertical", "modality": "shall-if-practicable", "prescribed_by": { "id": "30d-red", "cite": "30(d)" } } ] } ]
  },
  "modality_key": { "shall": "mandatory", "shall-if-practicable": "mandatory where practicable, with a stated fallback" },
  "cited_paragraphs": { "26(c)(i)": "…", "30(d)": "…", "30(a)": "…", "30(b)": "…" }
}

Rule 26(a) says a fishing vessel shows only the lights of that Rule; the anchor lights of 30(a)/(b) are struck and the response says which entry struck them. The excluded paragraphs are still cited verbatim so the veto can be checked, not just believed.

Response shape

Three properties, each tested in test/examples.test.ts:

  1. Cited, verbatim. Every entry id travels with its paragraph cite, and cited_paragraphs holds the text of every paragraph a response cites, straight from colregs data/rules.json. This package writes no prose of its own into a response; the only sentences in it are the Rules'.

  2. Modality per light. shall, may, shall-if-practicable and the rest are kept on each light, not summarised per display. Option 3 above is two shall lights and two may lights, and it says so four times. modality_key carries colregs' own one-line glossary for the modalities the response uses.

  3. Plural by construction. lawful_displays opens with count and relation (exactly_one or any_one_of) before any lights appear; each option numbers itself n of count and names the choice entries that distinguish it; and there is no top-level lights field to mistake for the answer. The tool description says why plural is correct, in the terms above.

A vessel that lawfully shows nothing gets one empty display, not an error, matching the engine. An error is reserved for input outside the vocabulary.

Every response carries colregs.version and, on evaluate_display, colregs.source, both straight from the engine. The server refuses to start if its own copy of colregs and the engine's are different releases.

Why a separate package

colregs-engine is pure, total and has no runtime dependencies, and formal-methods work is planned on top of it. A transport and a server SDK belong somewhere else. This is that somewhere.

Dependencies and stability

  • colregs-engine is a git dependency on that repository's main branch, because the engine is not yet published to npm. package-lock.json records the commit an install resolved; npm update colregs-engine moves it to the current tip. Breakage from an engine change is accepted at this stage.

  • colregs is the published data package. The engine and this package must resolve the same release; a split install fails at startup with a message saying so.

  • Version 0.0.x. Tool names, argument names and response fields may all change. The three response properties above are the commitment; nothing else is.

Licence

Apache-2.0. Nothing here is advice to mariners; the fitness-for-navigation disclaimer in colregs carries over.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mark-brannan/colregs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server