colregs-mcp
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 buildThen register the stdio server with your client. Claude Code:
claude mcp add colregs -- node /absolute/path/to/colregs-mcp/dist/cli.jsAny other MCP client: command node, argument dist/cli.js, no
environment.
Tools
tool | input | returns |
|
| every lawful display, cited and with modality per light |
|
| the applicability entries that hold, as |
|
| verbatim paragraph text from colregs |
|
| a light's definition from colregs |
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:
Cited, verbatim. Every entry id travels with its paragraph cite, and
cited_paragraphsholds the text of every paragraph a response cites, straight from colregsdata/rules.json. This package writes no prose of its own into a response; the only sentences in it are the Rules'.Modality per light.
shall,may,shall-if-practicableand the rest are kept on each light, not summarised per display. Option 3 above is twoshalllights and twomaylights, and it says so four times.modality_keycarries colregs' own one-line glossary for the modalities the response uses.Plural by construction.
lawful_displaysopens withcountandrelation(exactly_oneorany_one_of) before any lights appear; each option numbers itselfn of countand names the choice entries that distinguish it; and there is no top-levellightsfield 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-engineis a git dependency on that repository'smainbranch, because the engine is not yet published to npm.package-lock.jsonrecords the commit an install resolved;npm update colregs-enginemoves it to the current tip. Breakage from an engine change is accepted at this stage.colregsis 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
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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