PhoenixKit.API.MCP.Versions (phoenix_kit v2.63.0)

Copy Markdown View Source

The MCP protocol revisions this server knows, and which of them it speaks.

There is ONE table of known revisions (@table, newest first). Each revision has a profile: a map of feature flags. Everything that depends on the revision asks this module (profile/1, feature?/2) instead of comparing revision strings, so a revision is one row here and nowhere else.

Flags

  • :structured_content - a tools/call result carries structuredContent;
  • :output_schema - a tool descriptor carries outputSchema;
  • :tool_title - a tool descriptor carries title;
  • :server_title - serverInfo carries title.

Batching is not a flag: this server refuses a JSON-RPC batch under every revision (PhoenixKit.API.MCP.Rpc).

Which revisions are served

By default every known revision. The application environment narrows and orders that set, read at call time (so it needs no recompile and no restart of anything but the config change):

config :phoenix_kit, :mcp_protocol_versions, ["2025-06-18", "2025-03-26"]

The list is in preference order: latest/0 is its head, and negotiate/1 answers with it when the client asks for something this server does not serve. An entry that is not in the table is ignored; a list that ends up empty (or is not a list) is replaced by the default. Each of these is logged once per node, as a warning that names the key and never the value.

A revision that is known but not enabled is answered like any unsupported one: HTTP 400 with JSON-RPC -32600 on the MCP-Protocol-Version header, and a lower revision from initialize.

Adding or retiring a revision

Retire one without a code change: leave it out of :mcp_protocol_versions. Keep serving an older one when a newer one is added to the table: list the older ones there.

Adding a revision is one @table entry with its flags, when its wire format is one the code already produces (the same envelope, the same handshake, the same result and descriptor shapes, differing at most in which of the flags above are on). A revision that changes the wire format in any other way (a different handshake, a new required field, another message shape; 2026-07-28 is such a revision) cannot be added by data: it needs code in the places that read the flags (PhoenixKit.API.MCP, PhoenixKit.API.MCP.Shape, PhoenixKit.API.MCP.Rpc), and a new flag where the code must branch.

For hosts and tests

config :phoenix_kit, :mcp_extra_revisions, [{"2027-01-01", %{...flags}}] adds revisions to the table, for a host that has to speak one before core does and for the tests of this mechanism. Flags that are left out are off; an entry with a bad version, bad flags or the version of a built-in row is ignored (the built-in row wins), with the same one-time warning.

Summary

Functions

Whether the profile of version has flag on. A revision that is not known has none.

The feature flags a profile has.

The revision a request without an MCP-Protocol-Version header is taken to be.

Every revision in the table (the built-in ones and :mcp_extra_revisions), newest first.

The most preferred revision this server speaks: the newest unless the configuration orders otherwise.

The revision initialize answers with: what the client asked for when this server speaks it, else latest/0. Any term is accepted.

The profile of a known revision (enabled or not), nil for any other term.

The revisions this server speaks, in preference order.

Whether this server speaks version.

Types

flag()

@type flag() :: :structured_content | :output_schema | :tool_title | :server_title

profile()

@type profile() :: %{required(flag()) => boolean()}

version()

@type version() :: String.t()

Functions

feature?(version, flag)

@spec feature?(term(), flag()) :: boolean()

Whether the profile of version has flag on. A revision that is not known has none.

flags()

@spec flags() :: [flag()]

The feature flags a profile has.

implied()

@spec implied() :: version()

The revision a request without an MCP-Protocol-Version header is taken to be.

known()

@spec known() :: [version()]

Every revision in the table (the built-in ones and :mcp_extra_revisions), newest first.

latest()

@spec latest() :: version()

The most preferred revision this server speaks: the newest unless the configuration orders otherwise.

negotiate(requested)

@spec negotiate(term()) :: version()

The revision initialize answers with: what the client asked for when this server speaks it, else latest/0. Any term is accepted.

profile(version)

@spec profile(term()) :: profile() | nil

The profile of a known revision (enabled or not), nil for any other term.

supported()

@spec supported() :: [version()]

The revisions this server speaks, in preference order.

Every known revision unless :mcp_protocol_versions narrows or orders them. Never empty.

supported?(version)

@spec supported?(term()) :: boolean()

Whether this server speaks version.