SWI-Prolog MCP server and service - Reply # 01

It doesn’t work with Codex:

MCP client for `SWI-Prolog` failed to start: MCP startup failed: handshaking with MCP server failed: Send message error Transport
  [rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<codex_rmcp_client::http_client_adapte
  r::StreamableHttpClientAdapter>>] error: Transport channel closed, when send initialized notification

MCP startup incomplete (failed: SWI-Prolog)

Its analysis:

The actual failure is a protocol bug in the SWI-Prolog MCP server.

  Evidence:

  - Codex is configured correctly at /home/XXXX/.codex/config.toml:70.
  - The MCP initialize request succeeds and returns a session ID.
  - On notifications/initialized, the server returns:

  HTTP/1.1 200 OK
  Content-Type: application/json
  Content-Length: 0

  For a notification with no response body, Streamable HTTP requires 202 Accepted with no body. Returning 200 application/json with an empty body
  likely makes Codex’s RMCP worker attempt to parse nonexistent JSON, terminate, and report “Transport channel closed.” MCP transport specification
  (https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#sending-messages-to-the-server)

  The proper server response is:

  HTTP/1.1 202 Accepted
  Mcp-Session-Id: ...
  Content-Length: 0

  There is also a version smell: the server identifies itself as swipl-help 0.1.0 and negotiates protocol 2024-11-05, despite serving the newer
  Streamable HTTP endpoint. That may indicate an older or incomplete implementation, though version downgrade itself is permitted. MCP
  lifecycle/version negotiation (https://modelcontextprotocol.io/specification/2025-03-26/basic/lifecycle#version-negotiation)

Thanks. Please give it another try. I don’t have an OpenAI subscription any longer. Claude acknowledges this was a violation of the protocol that Claude itself apparently tolerates.

It’s happy now:

MCP Tools
• SWI-Prolog
  • Auth: Unsupported
  • Tools: apropos, c_function_defs, c_grep, explain, file_autoload, file_check, file_xref, git_blame, git_diff, git_grep, git_log, git_show, locate_source,
    manual_section, module_exports, pack_info, pldoc_signature, predicate_callers, predicate_help, predicate_info, predicate_listing, predicate_property_summary,
    source_list, source_read, source_roots, source_search

Can you tell me how SWI-Prolog implements atom_codes/2?

• atom_codes/2 is a C built-in, but the predicate itself is only a thin wrapper:

  static
  PRED_IMPL("atom_codes", 2, atom_codes, PL_FA_ISO)
  {
      PRED_LD
      return x_chars("atom_codes", A1, A2, X_ATOM);
  }

  The shared x_chars() routine also implements atom_chars/2, number_codes/2, name/2, and related predicates. Its effective logic for atom_codes/2 is:

  if Atom can be obtained as text:
      convert that text into a code list
      unify it with Codes
  else:
      read and validate Codes as text
      intern that text as an atom
      unify it with Atom

  More concretely:

  1. Atom → codes

     PL_get_text(Atom, &atext, CVT_ATOMIC) obtains a PL_chars_t, SWI-Prolog’s internal encoding-neutral text descriptor.

     PL_unify_text(Codes, 0, &atext, PL_CODE_LIST) then walks the text by Unicode code point and constructs/unifies the corresponding Prolog list.

  2. Codes → atom

     PL_get_text(Codes, &stext, CVT_STRING|CVT_LIST|CVT_EXCEPTION) traverses and validates the list. Each element must be a valid Unicode scalar value.

     PL_unify_text(Atom, 0, &stext, PL_ATOM) creates or looks up the interned atom and unifies it with the first argument.

  Internally, PL_chars_t may refer to Latin-1, UTF-8, or wide-character storage. Consequently, code lists represent Unicode code points rather than encoded bytes:

  ?- atom_codes('😀', Codes).
  Codes = [128512].

  Invalid code points, surrogate values, variables within an input list, and improper lists are diagnosed during PL_get_text():

  ?- atom_codes(_, [55296]).
  ERROR: Type error: `character_code' expected, found `55296'

  ?- atom_codes(_, [97|foo]).
  ERROR: Type error: `list' expected, found `[97|foo]'

  An interesting implementation detail is the temporary foreign frame around the Atom → codes attempt. If unifying the generated list fails after partially binding the
  supplied list, PL_discard_foreign_frame() rolls those bindings back before the reverse-direction attempt. This preserves normal Prolog unification semantics.

  Another SWI-specific detail: although the documented first argument is an atom, the forward path uses CVT_ATOMIC, which includes atoms, strings, and numbers. Thus
  current SWI-Prolog accepts:

  ?- atom_codes(42, Codes).
  Codes = [52, 50].

  ?- atom_codes("hi", Codes).
  Codes = [104, 105].

  The reverse direction still constructs an atom.

  Sources: current pl-prims.c implementation (https://github.com/SWI-Prolog/swipl-devel/blob/72e253b3896fc0021e4b50542bbd580197569d42/src/pl-prims.c), official atom_codes/2 documentation
  (https://www.swi-prolog.org/pldoc/doc_for?object=atom_codes/2), and SWI-Prolog character representation (https://www.swi-prolog.org/pldoc/man?section=chars).

This is with the as-yet-not-public gpt-5.6-sol, medium reasoning level.

Actually, scrub that - it didn’t actually use the MCP server to answer that :roll_eyes:


• You’re right. I overlooked the SWI-Prolog MCP because its tools were lazily exposed rather than visible in my initial tool list.

  I’ve now found and used it. It identifies:

  - Source roots: swipl and swipl-source
  - Implementation: src/pl-prims.c:4235
  - Delegation: x_chars("atom_codes", A1, A2, X_ATOM) at line 4237
  - Built-in registration at line 6351

  So my earlier “no SWI-Prolog MCP was available” statement was incorrect. Sorry about that.

Does it change your answer?

• No substantive change. The MCP confirms the core explanation:

  - atom_codes/2 is implemented in src/pl-prims.c:4235.
  - It delegates to x_chars(..., X_ATOM).
  - x_chars() first tries PL_get_text(..., CVT_ATOMIC) and generates a PL_CODE_LIST.
  - It uses a foreign frame to roll back partial bindings if that unification fails.
  - In the reverse direction it reads the list with PL_get_text() and creates an atom using PL_unify_text(..., PL_ATOM).

  Two useful refinements from the MCP:

  - The acceptance of numbers and strings in the forward direction is confirmed by the explicit CVT_ATOMIC flag, although the manual only documents atoms.
  - Surrogate code points have specifically been rejected since SWI-Prolog 10.1.8; older versions may behave differently.

  So the original technical answer stands, but it should have been grounded in the available SWI-Prolog MCP from the outset.

Stupid :robot: