{"slug": "prolog-knowledge-base-over-http-json-api-with-an-openapi-description", "title": "Prolog knowledge base over HTTP/JSON API, with an OpenAPI description", "summary": "Charon, a new open-source tool, exposes Prolog knowledge bases as HTTP/JSON APIs or MCP servers, with an OpenAPI 3.1 description, using an embedded scryer-prolog engine. It allows any predicate documented with a PlDoc comment to be callable over either interface, and supports persistence, CI checks, and OpenAPI export. The project is built with Rust and requires a recent stable toolchain.", "body_md": "Charon exposes a Prolog knowledge base as an API. A `.pl` file is loaded into an embedded\n[scryer-prolog](https://github.com/mthom/scryer-prolog) engine; any predicate documented with a\n[PlDoc](https://www.swi-prolog.org/pldoc/man?section=modes) `%!` comment becomes callable over\nwhichever interface you start:\n\n- `charon http` — an**HTTP/JSON API** , described by an OpenAPI 3.1 document at`/openapi.json`\n- `charon mcp` — an**MCP server** , over HTTP at`POST /mcp` or over stdio with`--stdio`\n\nBoth are rendered from one description of one operation set, so a predicate exposed on either is exposed identically on the other. Which one a process speaks is chosen when it starts, so what a port answers is a property of how you launched it rather than something a client discovers by probing.\n\n- A recent stable Rust toolchain (edition 2024).\n- Network access on first build: `scryer-prolog` is a git dependency compiled from source, which\ntakes a while.\n\n```\ncargo build\ncharon http kb.pl                     # REST + /openapi.json on 127.0.0.1:3000\ncharon http kb.pl --port 8080         # a different port\ncharon http kb.pl --host 0.0.0.0      # reachable from other machines (see below)\ncharon mcp  kb.pl                     # MCP at POST /mcp on 127.0.0.1:3000\ncharon mcp  kb.pl --stdio             # MCP over stdin/stdout, for a desktop client\ncharon http kb.pl --persist           # write assertz/retract changes back to the file\ncharon check kb.pl                    # load, report, exit — for CI\ncharon openapi kb.pl > openapi.json   # print the document and exit\n```\n\n`--persist` works on every subcommand. `--stdio` exists only under `mcp`, and cannot be combined\nwith `--host`/`--port`.\n\nThe default bind address is loopback. There is **no authentication**: passing `--host 0.0.0.0`\nmakes the whole knowledge base queryable by anyone who can reach the port, and Charon logs a\nwarning when you do.\n\nDiagnostics go to stderr on every subcommand — including `openapi`, so redirecting stdout gives\nyou a document and not a document with a log line in it. `--log` takes a `tracing` filter\n(`--log debug`, `--log charon=trace,warn`); `RUST_LOG` overrides it.\n\nIf the knowledge base fails to load, Charon exits rather than starting: a failed consult in\nscryer leaves *every* predicate undefined, so a server that started anyway would answer nothing.\n\n```\n{\n  \"mcpServers\": {\n    \"charon\": {\n      \"command\": \"/path/to/charon\",\n      \"args\": [\"mcp\", \"/path/to/knowledge_base.pl\", \"--stdio\"]\n    }\n  }\n}\n```\n\nTool names are the predicate names. A `get_`/` put_`/` delete_` prefix becomes a\n`readOnlyHint`/` idempotentHint`/` destructiveHint` annotation rather than being stripped. The\nknowledge base's source is offered as a resource at `charon://source`.\n\nAny predicate you want exposed needs a PlDoc mode line directly above its clauses:\n\n```\n%! pim_check(+Age:int, +Drugs:list(atom), -Substance:atom, -Reason:string) is nondet.\n%\n%   Enumerates one solution per criterion triggered by a patient's medication list.\n%\n%   @arg Age Age in completed years.\n%   @arg Drugs The substances to check.\n%   @arg Substance The substance a criterion fired on.\n%   @arg Reason Why it fired.\npim_check(Age, Drugs, Substance, Reason) :-\n    Age >= 65,\n    member(Substance, Drugs),\n    pim(Substance, Reason).\n```\n\nThe declared type decides how JSON becomes a Prolog term, in both directions and at every depth.\n\n| Declared | JSON | Prolog | \n|---|---|---|\n| `atom` | string | `'value'` — unifies with plain facts like`drug(aspirin)` | \n| `string` (or`text` ) | string | `\"value\"` — a character list | \n| `int` /`integer` | number | integer | \n| `float` /`number` | number | float | \n| `bool` /`boolean` | boolean | `true` /`false` | \n| `list(T)` | array | list of `T` , recursively | \n| `list` | array | same as `list(any)` | \n| `any` | any scalar or array | strings become atoms, numbers stay numbers | \n\nDeclaring the element type is what lets `?Drugs=[\"diazepam\"]` unify with ordinary atom facts. Only\n`+` (input) and `-` (output) modes can be exposed; `?` and `@` cannot, and neither can `compound`,\n`stream`, or a custom type. If a documented predicate does not appear, the server says why at\nstartup — `charon check kb.pl` prints the same report without binding anything.\n\nA zero-arity predicate is written without parentheses: `%! ready is semidet.`\n\n`GET` and `DELETE` take their arguments in the query string, `POST`/` PUT`/` PATCH` in a JSON body,\nand that is what the OpenAPI document describes. At runtime both are accepted for every method,\nso `curl -G` and `curl --json` both work — but giving the same argument twice is an error rather\nthan a silent precedence rule.\n\nA query string has no types, so `?Age=82` is read as an integer because `Age` was declared `int`.\nA JSON body is already typed and is passed straight through.\n\nOne solution is shaped by the predicate's output arguments:\n\n- **no output arguments** →`true`\n- **one output argument** → that value, unwrapped\n- **several** → an object keyed by argument name, in the order the mode line declares them\n\nWhether the response is that solution or an **array** of them follows the *declared determinism*,\nnot how many solutions turned up:\n\n| Declared | Response | \n|---|---|\n| `det` | the solution | \n| `semidet` | the solution, or `null` (`false` if there are no output arguments) | \n| `nondet` ,`multi` ,`failure` , undeclared | an array, one entry per solution, `[]` if the query failed | \n\nDeciding this from the declaration is what makes it unambiguous. When the shape depended on the\nanswer count, one solution binding `[1, 2]` and two solutions binding `1` and `2` came back as\nexactly the same JSON, and no client could tell them apart. It also means the OpenAPI response\nschema is exact rather than a `oneOf` covering every shape the runtime might reach.\n\nCharon wraps every generated goal in `catch/3`, so a Prolog exception is reported as an error\nresponse and the interpreter stays usable. Knowledge bases do not need their own guards.\n\nArguments are interpolated into generated Prolog source, and every value is escaped — quotes, backslashes and control characters included — so text that closes its own quote comes back as text rather than being executed.\n\nA `/** <module> Title ... */` comment anywhere in the file sets the OpenAPI `info` block and the\nMCP server identity.\n\nWith `--persist`, `assertz` and `retract` against a `:- dynamic(name/arity).` predicate are\nmirrored back into the source file after every call. Only single-line ground fact clauses are\nrewritten; rules, multi-line clauses and anything else are left exactly as written. The file is\nreplaced atomically (write to a sibling temporary file, then rename), so an interrupted write\ncannot leave a truncated knowledge base. Charon checks the file is writable at startup rather\nthan after the first change.\n\n```\ncargo test\ncargo test exception_does_not_poison_later_requests\n```\n\nMIT — see [LICENSE](/Christopher22/charon/blob/main/LICENSE).", "url": "https://wpnews.pro/news/prolog-knowledge-base-over-http-json-api-with-an-openapi-description", "canonical_source": "https://github.com/Christopher22/charon", "published_at": "2026-09-08 09:22:20+00:00", "updated_at": "2026-09-08 09:32:28.154942+00:00", "lang": "en", "topics": ["developer-tools", "ai-infrastructure"], "entities": ["Charon", "scryer-prolog", "PlDoc", "OpenAPI"], "alternates": {"html": "https://wpnews.pro/news/prolog-knowledge-base-over-http-json-api-with-an-openapi-description", "markdown": "https://wpnews.pro/news/prolog-knowledge-base-over-http-json-api-with-an-openapi-description.md", "text": "https://wpnews.pro/news/prolog-knowledge-base-over-http-json-api-with-an-openapi-description.txt", "jsonld": "https://wpnews.pro/news/prolog-knowledge-base-over-http-json-api-with-an-openapi-description.jsonld"}}