mcp_server

MCP (Model Context Protocol) server library for Logtalk applications. Makes any Logtalk application available as a local MCP server using stdio transport or Streamable HTTP transport.

API documentation

Open the ../../apis/library_index.html#mcp-server link in a web browser.

Loading

To load this library, load the loader.lgt file:

| ?- logtalk_load(mcp_server(loader)).

Testing

To test this library predicates, load the tester.lgt file:

| ?- logtalk_load(mcp_server(tester)).

This runs the 2025-06-18, 2025-11-25, 2026-07-28, stdio, and Streamable HTTP test sets.

For live tests, you can use e.g. MCPJam, Postman, or VSCode as clients. These and other tools usually provide reliable support for the 2025 family specs and the stdio transport. For the 2026-07-28 spec and the Streamable HTTP transport, support is currently more flaky but expected to evolve as implementations mature. Use preferably the latest versions of the clients for testing.

Architecture

This library currently supports three specs (aka data layers) and two transport bindings (stdio and Streamable HTTP, with optional SSE for progress and long-lived subscriptions):

  • 2025-06-18 (default) - tools, prompts, resources, completions, synchronous elicitation, structured output, resource links, version negotiation. Spec object: mcp_server_2025_06_18_spec.

  • 2025-11-25 - extends the 2025-06-18 handler with optional serverInfo.description, icons metadata on tools/prompts/resources (SEP-973), URL-mode elicitation (SEP-1036), and pass-through of enriched enum / form schemas (SEP-1330). Spec object: mcp_server_2025_11_25_spec (extends mcp_server_2025_06_18_spec).

  • 2026-07-28 - discovery, tools/prompts/resources/completions, multi-round tool results (MRTR), caching, progress, subscriptions, cancellation. Spec object: mcp_server_2026_07_28_spec.

The stdio transport is implemented by the mcp_server_stdio_transport object. The Streamable HTTP transport is implemented by the mcp_server_streamable_http_transport object (assumes a multi-threaded backend, which is required for subscriptions).

The stdio transport implement simple synchronous handling of server requests (by delegating to the used spec object). As a consequence, client notifications/cancelled can only be used to drop matching subscription entries (i.e., listen cancel) and cannot cancel in-flight work.

The Streamable HTTP transport supports request-scoped SSE progress and subscription fan-out via notify/1.

Spec references:

The library uses the json_rpc library for JSON-RPC 2.0 message handling.

This library is designed to support adding new MCP specs (aka data layers) and transport bindings by implementing mcp_server_spec_protocol and mcp_server_transport_protocol. Common server code is provided using the mcp_server_application category.

A facade object, mcp_server, allows selecting specific spec and transport using the spec/1 and transport/1 options.

The Streamable HTTP transport additionally depends on the http_server library (and optionally http_sse helpers) for listening and framing.

There’s also a set of protocols for the different MCP facets:

  • mcp_tool_protocol

  • mcp_completion_protocol

  • mcp_prompt_protocol

  • mcp_resource_protocol

  • mcp_multiround_protocol (2026 MRTR)

  • mcp_cache_protocol (2026 cache policy)

An application object is only required to implement protocols for the features it provides.

stdio versus Streamable HTTP transports

Both transports implement mcp_server_transport_protocol. What differs is how JSON-RPC is carried and a few transport-only features.

stdio transport

Streamable HTTP transport

Objects

mcp_server_stdio_transport

mcp_server_streamable_http_transport

Transport

Process stdin/stdout (newline-delimited JSON-RPC)

HTTP POST to a path (default /mcp)

Spec versions

2025-06-18, 2025-11-25, or 2026-07-28

2025-06-18 (*), 2025-11-25 (*), or 2026-07-28

Client model

Client spawns the server as a subprocess

Client talks to a listening URL

I/O in start/4

Reads and writes the given streams

Opens an http_server listener; stream arguments are unused

Progress

Stdio notifications when applicable

Optional SSE (text/event-stream) when a progressToken is present

Subscriptions

Stdio listen loop

Long-lived SSE plus notify/1 fan-out

Extra options

Spec options (instructions, cache_*, …)

Plus http_* options

(*) But no synchronous elicitation over plain POST.

Application objects do not change between transports. Only the spec/1 and transport/1 options selects the path:

% stdio, 2025-06-18 (default)
spec('2025-06-18'), transport(stdio)

% stdio, 2025-11-25
spec('2025-11-25'), transport(stdio)

% stdio, 2026-07-28
spec('2026-07-28'), transport(stdio)

% Streamable HTTP, 2025-06-18
spec('2025-06-18'), transport(streamable_http)

% Streamable HTTP, 2025-11-25
spec('2025-11-25'), transport(streamable_http)

% Streamable HTTP, 2026-07-28
spec('2026-07-28'), transport(streamable_http)

Use stdio with desktop MCP clients that launch a command and speak MCP on pipes. Use Streamable HTTP for remote or multi-client access, reverse proxies, or clients that POST JSON-RPC (with optional SSE for progress and subscriptions).

Always start servers through the mcp_server facade. For unit tests or an external HTTP stack, the Streamable HTTP transport also exposes prepare/2, handle_mcp_request/4, and cleanup/0 predicates without opening a listener.

Starting a MCP server

Starting a MCP server requires at least a server name and the application server object and optionally a list of options to customize the server. Always use the mcp_server facade object to start a server (the spec and transport objects are not meant to be used directly). Some examples, assuming a my_tools application object:

2025-06-18 spec and stdio transport (default)

| ?- mcp_server::start('my-server', my_tools).

With options:

| ?- mcp_server::start('my-server', my_tools, [
        server_version('2.0.0'),
        server_title('My Server')
    ]).

2025-11-25 spec and stdio transport

| ?- mcp_server::start('my-server', my_tools, [
        spec('2025-11-25'),
        server_version('2.0.0'),
        server_title('My Server'),
        server_description('Optional human-readable server description.')
    ]).

2026-07-28 spec and stdio transport

| ?- mcp_server::start('my-server', my_tools, [
        spec('2026-07-28'),
        server_version('2.0.0'),
        server_title('My Server'),
        instructions('Optional server instructions for clients.'),
        cache_ttl(0),
        cache_scope(private)
    ]).

For stdio transports there should either be no standard output or only a Prolog backend term input prompt. Spurious standard output will break the connection between an MCP client and the MCP server.

2026-07-28 spec and Streamable HTTP

| ?- mcp_server::start('my-server', my_tools, [
        spec('2026-07-28'),
        transport(streamable_http),
        server_version('2.0.0'),
        server_title('My Server'),
        instructions('Optional server instructions for clients.'),
        http_port(8080),
        http_bind('127.0.0.1'),
        http_path('/mcp'),
        http_origin_check(true)
    ]).

The server listens for POST requests at the configured bind address, port, and path (default http://127.0.0.1:8080/mcp). Clients must send MCP-Protocol-Version: 2026-07-28 and a JSON-RPC body using the 2026 _meta conventions. When a progressToken is present, the response may use text/event-stream (SSE) for progress events and the final result.

For unit tests and embedded HTTP stacks, call mcp_server_streamable_http_transport::prepare/2 then handle_mcp_request/4 without starting the listener, and finish with cleanup/0.

Common options

Option

Default

Description

spec(Spec)

'2025-06-18'

Spec selection

transport(Transport)

stdio

Transport selection

server_version(Version)

'1.0.0'

Server version string

server_title(Title)

'logtalk-mcp-server'

Display title

server_description(Text)

''

Optional serverInfo.description (2025-11-25; also accepted by 2025-06-18)

2026-07-28 spec specific options

Option

Default

Description

instructions(Text)

''

Optional instructions (2026 discover)

cache_ttl(Milliseconds)

0

Default TTL in milliseconds (2026)

cache_scope(Scope)

private

public or private (2026)

Streamable HTTP transport options

Option

Default

Description

http_port(Port)

8080

TCP port to listen on

http_bind(Address)

'127.0.0.1'

Bind address

http_path(Path)

'/mcp'

HTTP path for MCP POST requests

http_origin_check(Flag)

true

Reject disallowed Origin headers when true

http_sse_keepalive(Seconds)

15

Keep-alive interval for subscriptions/listen response streams

http_server_options(Options)

[]

Options passed to http_server::serve_until_shutdown/5

oauth(Verifier, ProtectedResource, MetadataDescriptors, ProtectOptions)

none

Enable OAuth protection and RFC 9728 metadata publication

These options are validated by the mcp_server facade and applied only when transport(streamable_http) is used.

The http_server_options/1 list selects the HTTP scheme, server transport, and listener configuration using the http_server library option vocabulary. The default empty list uses that library’s defaults: plain HTTP and its default HTTP transport. The MCP transport adds workers(per_connection) so long-lived subscriptions do not block other requests.

For a direct HTTPS listener, provide a certificate and private key:

| ?- mcp_server::start('my-server', my_tools, [
        transport(streamable_http),
        http_port(8443),
        http_server_options([
            scheme(https),
            tls_certificate_file('/path/to/cert.pem'),
            tls_key_file('/path/to/key.pem')
        ])
    ]).

For local testing, replace the certificate and key options with temporary_tls_credentials(Prefix). The http_server library selects its HTTPS-capable default transport and validates scheme, transport, and TLS credential consistency when opening the listener.

OAuth protection

Use the oauth/4 option to protect all requests to a Streamable HTTP MCP server. OAuth is supported for all three MCP spec versions. It is not supported by the stdio transport.

| ?- mcp_server::start('my-server', my_tools, [
        spec('2026-07-28'),
        transport(streamable_http),
        http_port(8443),
        http_server_options([
            scheme(https),
            tls_certificate_file('/path/to/cert.pem'),
            tls_key_file('/path/to/key.pem')
        ]),
        oauth(
            http_oauth_jwt_verifier(PublicJWKSet, [
                allow_algorithms(['RS256']),
                claim_policy([
                    claim(iss, expected('https://identity.example.com'))
                ])
            ]),
            'https://api.example.com/mcp',
            [
                authorization_servers(['https://identity.example.com']),
                scopes_supported([mcp_access]),
                resource_name('My MCP Server')
            ],
            [required_scopes([mcp_access])]
        )
    ]).

The verifier must implement http_oauth_verifier_protocol. JWT and token introspection verifiers supplied by the http_oauth library can be used directly. The protected resource must be its canonical externally visible HTTPS URL and must match the audience accepted by the verifier.

The metadata descriptors are passed to http_oauth_metadata with required_members([authorization_servers]). The server automatically serves the resulting public RFC 9728 protected-resource metadata document at the well-known URL derived from the protected resource. For the example above, the URL is https://api.example.com/.well-known/oauth-protected-resource/mcp.

Protection options use the http_oauth option vocabulary, including required_scopes/1, scope_checker/1, realm/1, headers/1, body/1, and properties/1. The protected_resource/1 and resource_metadata/1 options are reserved and derived automatically. Required scopes are static and apply to every MCP HTTP request.

Every protected HTTP request, including requests associated with an MCP session and long-lived SSE requests, must carry a valid Bearer token. Missing, malformed, invalid, and insufficient-scope credentials are rejected before an SSE response is started. The raw token is not retained in the request passed to the MCP handler.

Implementing the tool protocol

To expose a Logtalk object as an MCP tool provider, implement the mcp_tool_protocol protocol. For example:

:- object(my_tools,
    implements(mcp_tool_protocol)).

    :- public(factorial/2).
    :- mode(factorial(+integer, -integer), one).
    :- info(factorial/2, [
        comment is 'Computes the factorial of a non-negative integer.',
        argnames is ['N', 'F']
    ]).

    :- uses(natural, [
        factorial/2
    ]).

    tools([
        tool(factorial, factorial, 2)
    ]).

:- end_object.

The tools/1 predicate returns a list of tool(Name, Functor, Arity) descriptors. Tool descriptions, input schemas, and output schemas are derived from the info/2 and mode/2 directives. Input-mode arguments (+, ++, and @) define inputSchema; output-mode arguments (- and --) define outputSchema. A title key in the predicate’s info/2 directive provides a human-friendly display name. If omitted, the predicate functor is used.

Supported Logtalk types and their JSON Schema counterparts:

Logtalk type

JSON type

integer

integer

float

number

number

number

atom

string

boolean

boolean

list

array

list(_)

array

compound

object

nonvar

string

term

string

chars

string

codes

string

(other)

string

When a tool predicate mode/2 directive uses types without a JSON counterpart, you can override the inferred input and output schemas (see below for details) if resorting to JSON strings is not ideal.

Auto-dispatch

By default the server auto-dispatches a tool call: it binds the input-mode arguments (+, ++, and @), calls the corresponding predicate, collects the output-mode arguments (- and --), and returns them as structuredContent. For backwards compatibility, it also returns the existing human-readable rendering as a text/1 content item. The tool descriptor’s inputSchema and outputSchema are inferred from the predicate documentation. If the predicate fails, the server returns an MCP tool error stating Tool predicate failed; if it throws an exception, the exception is returned as the tool error.

Overriding input/output schemas

Tools can override an inferred input or output schema by defining input_schema/2 or output_schema/2 in their application object:

input_schema(factorial, {
    type-object,
    properties-{'N'-{type-integer}}, required-['N']
}).

output_schema(factorial, {
    type-object,
    properties-{'F'-{type-integer}}, required-['F']
}).

The tool descriptor always includes inputSchema and outputSchema fields.

Custom input_schema/2 properties may include the MCP Streamable HTTP extension key x-mcp-header (SEP-2243). When present, conforming clients mirror that argument into an HTTP header Mcp-Param-{suffix} on tools/call. The value must be a non-empty atom; the property should be a top-level string, integer, or boolean. Example:

input_schema(execute_sql, {
    type-object,
    properties-{
        region-{type-string, 'x-mcp-header'-'Region'},
        query-{type-string}
    },
    required-[region, query]
}).

On Streamable HTTP under the 2026-07-28 spec, the transport validates Mcp-Param-* headers against the tool arguments for annotated properties (missing header or mismatched value yields a HeaderMismatch / invalid params error). Clients on stdio may ignore x-mcp-header. The annotation is optional for servers; including it in input_schema/2 is enough for listing — no extra protocol is required.

Defining output_schema/2 is usually required when a tool predicate has no arguments (which results in an inferred empty output schema) to ensure wide compatibility with clients. For example, assuming a text output that we want to ensure that is received by all clients:

output_schema(tool_predicate, {
    type-object,
    properties-{message-{type-string}},
    required-[message]
}).

Combine this with a tool_call/3 predicate definition (see below) that binds the result argument to either structured([text(Text)], {message-Text}) or structured({message-Text}).

Custom result formatting

For custom result formatting, including a structuredContent result matching the tool’s output schema, implement tool_call/3. As an output schema is always advertised, a successful custom result must use structured/1 or structured/2 and conform to that schema. The Result term can be:

  • structured(StructuredContent) - structured output with auto text

  • structured(Items, StructuredContent) - structured output with explicit content items

The content items can be:

  • text(Atom) - a text result

  • error(Atom) - a tool-level error (isError: true)

  • results(List) - content items (text/1, error/1, resource_link/2, resource_link/4)

The StructuredContent argument must be a curly-term matching the schema.

For example:

tool_call(factorial, Arguments, Result) :-
    member('N'-N, Arguments),
    factorial(N, F),
    number_codes(F, Codes),
    atom_codes(FAtom, Codes),
    atom_concat('The factorial is: ', FAtom, Text),
    Result = structured([text(Text)], {'F'-F}).

The tool_call/3 predicate is specially useful for tool predicates with no arguments. In this case, custom text can be used to explain success or failure of the tool predicate by returning a structured([text(Explanation)], {}) result. Note that {} is the output schema when a predicate have no output arguments.

Elicitation (2025-06-18 spec)

Under the 2025-06-18 spec, tools that need to ask the user question during execution can use MCP elicitation if the MCP client supports it (tested and working with VSCode Copilot). The application declares that it requires the client elicitation capability and implements tool_call/4 instead of tool_call/3. The extra argument is an elicitation closure. For example:

:- object(interactive_tools,
    implements(mcp_tool_protocol)).

    capabilities([elicitation]).

    tools([
        tool(ask_name, ask_name, 0)
    ]).

    :- public(ask_name/0).
    :- info(ask_name/0, [
        comment is 'Asks the user for their name and greets them.'
    ]).

    tool_call(ask_name, _Arguments, Elicit, Result) :-
        Schema = {
            type-object,
            properties-{name-{type-string}},
            required-[name]
        },
        call(Elicit, 'What is your name?', Schema, Answer),
        (   Answer = accept(Content),
            has_pair(Content, name, Name) ->
            atom_concat('Hello, ', Name, Greeting),
            atom_concat(Greeting, '!', Text),
            Result = text(Text)
        ;   Result = text('No name provided.')
        ).

    has_pair({Pairs}, Key, Value) :-
        curly_member(Key-Value, Pairs).

    curly_member(Pair, (Pair, _)) :- !.
    curly_member(Pair, (_, Rest)) :-
        !, curly_member(Pair, Rest).
    curly_member(Pair, Pair).

:- end_object.

The Elicit closure is called as call(Elicit, Message, Schema, Answer) where:

  • Message - an atom with the prompt text

  • Schema - a curly-term JSON Schema for the requested input

  • Answer - unified with accept(Content), decline, or cancel

When accept(Content) is returned, Content is a curly-term with the user’s response matching the requested schema.

See the examples/birds_mcp/ example for a complete demonstration of elicitation with a bird identification expert system.

Note that the 2026-07-28 spec never invokes tool_call/4. Multi-round interaction for the 2026-07-28 spec uses mcp_multiround_protocol instead.

2025-11-25 additions

The mcp_server_2025_11_25_spec object extends mcp_server_2025_06_18_spec and reuses the same lifecycle (initialize / notifications/initialized), stdio elicitation, and tool/prompt/resource methods. Select it with spec('2025-11-25').

Server description

Pass server_description(Text) so initialize includes serverInfo.description (Implementation.description in the spec). Empty or omitted description omits the field.

Icons (SEP-973)

Applications may define public predicates that return a list of icon objects for listing responses:

:- public(tool_icons/2).
tool_icons(factorial, [
    {src-'https://example.com/icons/factorial.png', mimeType-'image/png'}
]).

:- public(prompt_icons/2).
prompt_icons(code_review, [{src-..., mimeType-...}]).

:- public(resource_icons/2).
resource_icons('logtalk://app/data', [{src-..., mimeType-...}]).

Each icon is a curly-term. Common fields: src (required URL), optional mimeType, optional sizes (list of size atoms such as '48x48'). Icons are omitted when the predicate is undefined or fails. The first argument of resource_icons/2 can be a concrete resource URI or a resource URI template.

URL-mode elicitation (SEP-1036)

In addition to form elicitation via elicit_request/5 (inherited), the 2025-11-25 handler exposes elicit_url_request/5:

mcp_server_2025_11_25_spec::elicit_url_request(
    Input, Output,
    'Open the documentation to continue',
    'https://example.com/docs',
    Answer
).

This sends elicitation/create with mode: url and a url field. Answer is accept(Content), decline, or cancel, same as form mode. Tools that need URL elicitation during tool_call/4 can call this predicate from an application helper when stdio streams are available.

Enum / form schemas (SEP-1330)

Enriched requestedSchema / inputSchema shapes (for example enum with enumNames for titled choices) are pass-through: supply them in the schema curly-term you pass to the elicitation closure or in input_schema/2. The spec does not rewrite schema dialects.

Not implemented from 2025-11-25

  • Sampling with tools / toolChoice

  • Experimental Tasks (SEP-1686; later moved to an extension in 2026)

  • OAuth / OpenID Connect Discovery / Client ID Metadata Documents

Multi-round tool results - MRTR (2026-07-28 spec)

Implement mcp_multiround_protocol and define round hooks:

  • tool_call_round/4

  • prompt_get_round/4

  • resource_read_round/4

Each receives a request_context(ClientCapabilities, InputResponses, RequestState, Progress) term and returns either:

  • complete(Result) - using the existing canonical result vocabulary

  • input_required(InputRequests, RequestState) - request more input

InputRequests is a list of uniquely keyed input_request(Key, Request) terms. Allowed request forms:

  • form_elicitation(Message, Schema)

  • url_elicitation(Message, URL)

  • sampling(Messages, ModelPreferences, SystemPrompt, IncludeContext)

  • roots

RequestState is application-owned opaque data (or none). Applications that use it for authorization or business decisions must integrity-protect and validate it themselves.

Existing applications that do not implement the round hooks continue through tool_call/3, prompt_get/3, resource_read/3, or auto-dispatch; the 2026-07-28 spec wraps those outcomes as complete.

Example:

:- object(interactive,
    implements([mcp_tool_protocol, mcp_multiround_protocol])).

    tools([tool(ask_name, ask_name, 0)]).

    tool_call_round(ask_name, _Args, Context, RoundResult) :-
        Context = request_context(_Caps, Responses, State, _Progress),
        (   State == none ->
            RoundResult = input_required(
                [input_request(name_key, form_elicitation('Your name?', {type-object, properties-{name-{type-string}}, required-[name]}))],
                waiting
            )
        ;   member(input_response(name_key, accept(Content)), Responses) ->
            % extract name, return complete(text(...))
            RoundResult = complete(text('Hello!'))
        ;   RoundResult = complete(text('Cancelled.'))
        ).

:- end_object.

Caching (2026-07-28 spec)

Optional mcp_cache_protocol with cache_policy/4:

cache_policy(tools_list, _, 1000, private).
cache_policy(resources_templates_list, _, 1000, public).
cache_policy(resources_read, 'logtalk://app/data', 5000, public).

Cache fields (ttlMs, cacheScope) are attached only to complete results of server/discover, list operations, and resources/read. They are never attached to input_required results or MRTR retries.

Defaults: cache_ttl(0), cache_scope(private).

Progress (2026-07-28 spec)

When the client supplies a progressToken in request _meta, the Progress closure in request_context can emit notifications/progress. Progress is suppressed after cancellation or completion.

Subscriptions and notifications (2026-07-28 spec)

Clients call subscriptions/listen with filters. The server acknowledges first (notifications/subscriptions/acknowledged), then delivers matching events.

Applications publish events via:

mcp_server::notify(tools_list_changed).
mcp_server::notify(prompts_list_changed).
mcp_server::notify(resources_list_changed).
mcp_server::notify(resource_updated('logtalk://app/data')).

The facade delegates to the active spec. The 2025-06-18 spec ignores these events. The 2026-07-28 spec (with both stdio and Streamable HTTP transports) route them through active subscriptions. On HTTP, long-lived SSE connections from subscriptions/listen receive matching events; notify/1 isolates per-subscriber failures so a dead stream does not abort delivery to others.

Prompts

MCP prompts are templates for structured LLM interactions. They allow an application to expose reusable prompt templates that MCP clients can discover and use. To add prompts, implement mcp_prompt_protocol in addition to mcp_tool_protocol, and declare prompts in capabilities:

:- object(my_prompts,
    implements([mcp_tool_protocol, mcp_prompt_protocol])).

    :- uses(list, [member/2]).

    capabilities([prompts]).

    tools([]).

    prompts([
        prompt(code_review, 'Reviews code for potential issues', [
            argument(code, 'The code to review', true),
            argument(language, 'The programming language', false)
        ]),
        prompt(summarize, 'Summarizes a given text', [
            argument(text, 'The text to summarize', true)
        ])
    ]).

    prompt_get(code_review, Arguments, Result) :-
        (   member(code-Code, Arguments) ->
            atom_concat('Please review the following code for potential issues:\n\n', Code, Text)
        ;   Text = 'Please provide code to review.'
        ),
        Result = messages([message(user, text(Text))]).

    prompt_get(summarize, Arguments, Result) :-
        (   member(text-Text, Arguments) ->
            atom_concat('Please summarize the following text:\n\n', Text, PromptText)
        ;   PromptText = 'Please provide text to summarize.'
        ),
        Result = messages([message(user, text(PromptText))]).

:- end_object.

The prompts/1 predicate returns a list of prompt descriptors:

  • prompt(Name, Description, Arguments) - without title

  • prompt(Name, Title, Description, Arguments) - with title

Where:

  • Name - the MCP prompt name (an atom)

  • Title - a human-friendly display name (an atom, optional)

  • Description - a human-readable description (an atom)

  • Arguments - a list of argument(ArgName, ArgDescription, Required) terms where Required is true or false

The prompt_get/3 predicate handles prompt get requests. Its result term can be:

  • messages(MessageList) - a list of prompt messages

  • messages(Description, MessageList) - a list of messages with a description

Each message in the list is a message(Role, Content) term where:

  • Role - user or assistant

  • Content - text(Text) where Text is an atom

Multi-turn prompts can return multiple messages:

prompt_get(debate, Arguments, Result) :-
    member(topic-Topic, Arguments),
    atom_concat('Let us debate: ', Topic, UserText),
    Result = messages([
        message(user, text(UserText)),
        message(assistant, text('I would be happy to debate that topic. What is your position?'))
    ]).

For 2026 multi-round prompts, implement prompt_get_round/4.

Completions

MCP completions provide application-ranked suggestions for prompt arguments and resource URI or URI-template arguments. Implement mcp_completion_protocol together with the referenced prompt and resource protocols, and declare completions in capabilities/1:

:- object(my_completions,
    implements([
        mcp_tool_protocol,
        mcp_completion_protocol,
        mcp_prompt_protocol,
        mcp_resource_protocol
    ])).

    capabilities([completions, prompts, resources]).

    completion(prompt(code_review), language-Partial, _Context, Result) :-
        % Filter and rank using Partial.
        Result = completion([logtalk, prolog]).

    completion(resource('logtalk://my-app/users/{name}'), name-Partial, Context, Result) :-
        % Context contains previously resolved Name-Value pairs.
        Result = completion([alice, alicia], 5, true).

:- end_object.

The completion/4 arguments are:

  • Reference - prompt(Name) or resource(URIOrTemplate)

  • Argument - an ArgumentName-PartialValue pair

  • Context - previously resolved argument Name-Value pairs, or []

  • Result - completion(Values) or completion(Values, Total, HasMore)

Values must be an application-ranked list of atoms. Total is an optional non-negative count of all available matches and HasMore is true or false. The server preserves order and does not sort or deduplicate suggestions. When more than 100 values are returned, the server retains the first 100 and sets hasMore to true.

Prompt references and argument names must match descriptors returned by prompts/1. Resource references must exactly match a URI returned by resources/1 or a URI template returned by resource_templates/1; no RFC 6570 reverse matching is required. Invalid requests return -32602, while callback failures and invalid result terms return -32603. Calls made when completions is not advertised return -32601.

Applications are responsible for filtering suggestions according to caller authorization, preventing information disclosure, ranking domain results, and applying any domain-specific throttling. Completion is synchronous and does not use MRTR, progress notifications, or 2026 cache fields.

Resources

MCP resources expose data and content from the application that MCP clients can access. To add resources, implement mcp_resource_protocol in addition to mcp_tool_protocol, and declare resources in capabilities:

:- object(my_resources,
    implements([mcp_tool_protocol, mcp_resource_protocol])).

    capabilities([resources]).

    tools([]).

    resources([
        resource('logtalk://my-app/config', config, 'Application configuration', 'application/json'),
        resource('logtalk://my-app/readme', readme, 'Application readme', 'text/plain')
    ]).

    resource_templates([
        resource_template(
            'logtalk://my-app/users/{name}',
            user,
            'User record',
            'application/json'
        )
    ]).

    resource_read('logtalk://my-app/config', _Arguments, Result) :-
        Result = contents([
            text_content('logtalk://my-app/config', 'application/json', '{"name": "my-app", "version": "1.0"}')
        ]).

    resource_read('logtalk://my-app/readme', _Arguments, Result) :-
        Result = contents([
            text_content('logtalk://my-app/readme', 'text/plain', 'Welcome to my application.')
        ]).

    resource_read('logtalk://my-app/users/alice', _Arguments, Result) :-
        Result = contents([
            text_content('logtalk://my-app/users/alice', 'application/json', '{"name":"alice"}')
        ]).

:- end_object.

The resources/1 predicate returns a list of resource descriptors:

  • resource(URI, Name, Description, MimeType) - without title

  • resource(URI, Name, Title, Description, MimeType) - with title

Where:

  • URI - the resource identifier (an atom, typically a URI like logtalk://my-app/data)

  • Name - a human-readable name (an atom)

  • Title - a human-friendly display name (an atom, optional)

  • Description - a human-readable description (an atom)

  • MimeType - the MIME type of the resource content (an atom, e.g. 'text/plain', 'application/json')

The optional resource_templates/1 predicate returns parameterized resource descriptors:

  • resource_template(URITemplate, Name, Description, MimeType) - without title

  • resource_template(URITemplate, Name, Title, Description, MimeType) - with title

URITemplate is an RFC 6570 URI template atom. Templates are listed using resources/templates/list. The server validates the complete descriptor list before advertising it. An invalid template raises a domain_error(uri_template, URITemplate) error and no partial list is returned. For valid templates, the server checks the literal template segments before delegating a concrete URI to resource_read/3; the application remains responsible for validating template expressions, authorizing the concrete URI, and producing its contents.

The resource_read/3 predicate handles resource read requests. Its result term must be contents(ContentList) where each content item is:

  • text_content(URI, MimeType, Text) - for text resources

  • blob_content(URI, MimeType, Base64Data) - for binary resources encoded as base64

A resource can return multiple content items. For example:

resource_read('logtalk://my-app/logs', _Arguments, Result) :-
    Result = contents([
        text_content('logtalk://my-app/logs', 'text/plain', 'Log entry 1'),
        text_content('logtalk://my-app/logs', 'text/plain', 'Log entry 2')
    ]).

For 2026 multi-round reads, implement resource_read_round/4.

MCP client configuration

Example claude_desktop_config.json for the 2025-06-18 path:

{
    "mcpServers": {
        "my-server": {
            "command": "swilgt",
            "args": [
                "-q",
                "-g", "logtalk_load(my_mcp_server(loader))",
                "-t", "halt"
            ],
            "env": {
                "LOGTALKHOME": "/usr/local/share/logtalk",
                "LOGTALKUSER": "/Users/jdoe/logtalk"
            }
        }
    }
}

The env definition of the LOGTALKHOME and LOGTALKUSER environment variables may or may not be required (it’s usually necessary on macOS). When required, replace the values above with the actual values on your Logtalk setup.

The actual arguments to the integration script (swilgt in the example above) depend on the Prolog backend. For example, XVM requires instead:

{
    "mcpServers": {
        "my-server": {
            "command": "xvmlgt",
            "args": [
                "-q",
                "-g", "logtalk_load(my_mcp_server(loader)), halt.",
            ],
            "env": {
                "LOGTALKHOME": "/usr/local/share/logtalk",
                "LOGTALKUSER": "/Users/jdoe/logtalk"
            }
        }
    }
}

For a 2025-11-25 or 2026-07-28 stdio server, the application loader or start goal must pass the, respectively, spec('2025-11-25) or spec('2026-07-28) option.

For Streamable HTTP, start the server with the transport(streamable_http) and (preferably) spec('2026-07-28) options. Point the MCP client at the listen URL (for example http://127.0.0.1:8080/mcp).

Each 2026-07-28 request should include:

  • Content-Type: application/json

  • Accept: application/json, text/event-stream

  • MCP-Protocol-Version: 2026-07-28 (must match params._meta protocol version)

  • Mcp-Method: <json-rpc-method> (e.g. server/discover, tools/call)

  • Mcp-Name: <name-or-uri> when the method is tools/call, prompts/get, or resources/read (must match params.name or params.uri)

  • Mcp-Param-<suffix>: <value> when the tool’s inputSchema annotates a property with x-mcp-header and that argument is present in the body

Header mismatches use JSON-RPC error code -32020 (HeaderMismatch). Missing required metadata uses -32602.

Error handling

2025-06-18 spec

  • Predicate failures result in a tool-level error with isError: true.

  • Predicate exceptions result in a tool-level error with the exception term serialized as the error text.

  • Prompt execution failures result in a JSON-RPC error response.

  • Resource read failures result in a JSON-RPC error response.

2026-07-28 spec

Code

Meaning

-32602

Missing/malformed required metadata or invalid arguments

-32020

Header mismatch (MCP-Protocol-Version, Mcp-Method, Mcp-Name, Mcp-Param-*)

-32022

Unsupported protocol version (data.supported, data.requested)

-32021

Missing required client capability (data.requiredCapabilities)

-32601

Unknown or unadvertised method

-32603

Internal / execution failure

Protocols overview

Follows a list of the main predicates declared in the protocols meant to be implemented by an application. See the API documentation for full details.

mcp_tool_protocol

  • capabilities/1 - returns the list of additional features needed by the application (e.g. [elicitation], [prompts], [resources], [completions], or [completions, prompts, resources]); prompts, resources, and completions are server capabilities while elicitation is a required client capability; optional, defaults to []

  • tools/1 - returns the list of tool descriptors

  • tool_call/3 - handles a tool call (optional; auto-dispatch is used when not defined)

  • tool_call/4 - handles a tool call with an elicitation closure (optional; requires capabilities([elicitation]) or capabilities([..., elicitation]); 2025-06-18 only)

  • input_schema/2 - overrides the inferred JSON Schema for tool input (optional)

  • output_schema/2 - overrides the inferred JSON Schema for structured tool output (optional)

mcp_prompt_protocol

  • prompts/1 - returns the list of prompt descriptors

  • prompt_get/3 - handles a prompt get request

mcp_completion_protocol

  • completion/4 - completes a prompt argument or resource URI/template argument

mcp_resource_protocol

  • resources/1 - returns the list of resource descriptors

  • resource_templates/1 - optionally returns resource template descriptors

  • resource_read/3 - handles a resource read request

mcp_multiround_protocol (2026-07-28)

  • tool_call_round/4 - handles one round of a multi-round tool call

  • prompt_get_round/4 - handles one round of a multi-round prompt get

  • resource_read_round/4 - handles one round of a multi-round resource read

mcp_cache_protocol (2026-07-28)

  • cache_policy/4 - optional per-operation TTL and scope

Supported MCP methods per spec

2025-06-18 and 2025-11-25 specs

The 2025-11-25 handler supports the same methods as 2025-06-18. Listing responses may include optional icons. Form and URL modes of elicitation/create are both available under 2025-11-25.

Method

Type

Description

initialize

Request

Handshake and version negotiation

notifications/initialized

Notification

Client acknowledgment

ping

Request

Liveness check

tools/list

Request

List tools (optional icons)

tools/call

Request

Call a tool

prompts/list

Request

List prompts (optional icons)

prompts/get

Request

Get a prompt

completion/complete

Request

Complete a prompt/resource value

resources/list

Request

List resources (optional icons)

resources/templates/list

Request

List templates (optional icons)

resources/read

Request

Read a resource

elicitation/create

Request (server -> client)

Form or URL mode user input

2026-07-28 spec

Method

Type

Description

server/discover

Request

Discovery (replaces initialize)

tools/list

Request

List tools

tools/call

Request

Call a tool (supports MRTR)

prompts/list

Request

List prompts

prompts/get

Request

Get a prompt (supports MRTR)

completion/complete

Request

Complete a prompt/resource value

resources/list

Request

List resources

resources/templates/list

Request

List resource templates

resources/read

Request

Read a resource (supports MRTR)

subscriptions/listen

Request

Open a subscription

notifications/cancelled

Notification

Cancel in-flight request

notifications/progress

Notification (server -> client)

Progress update

notifications/subscriptions/acknowledged

Notification (server -> client)

Subscription ack

notifications/tools/list_changed

Notification (server -> client)

Tools changed

notifications/prompts/list_changed

Notification (server -> client)

Prompts changed

notifications/resources/list_changed

Notification (server -> client)

Resources changed

notifications/resources/updated

Notification (server -> client)

Resource updated

The ping method was removed in the 2026-07-28 specification; requests for it return method not found (-32601).

The stdio transport never writes JSON-RPC requests to stdout (only responses and notifications). The Streamable HTTP transport likewise only returns responses and server-initiated notifications (progress and subscription events), never client-bound requests over the HTTP response channel.

MCP Apps (interactive UI)

MCP Apps (io.modelcontextprotocol/ui) lets tools declare an interactive HTML UI that hosts render in a sandboxed iframe. The server only serves tools and ui:// resources; host <-> iframe traffic is handled by the host.

Compatible with 2025-06-18 and 2026-07-28, and with stdio and Streamable HTTP. Spec:

Declaring the extension

capabilities([resources, ui]).

Advertises extensions["io.modelcontextprotocol/ui"] in initialize (2025) or server/discover (2026).

UI resources

resources([
  resource(
    'ui://my-app/dashboard',
    dashboard,
    'Interactive dashboard',
    'text/html;profile=mcp-app'
  )
]).

Optional CSP via resource_ui_meta/2 (mcp_ui_protocol).

Linking tools to UI

tool_ui(show_dashboard, [
  resource_uri('ui://my-app/dashboard'),
  visibility([model, app])
]).

tools/list includes _meta.ui.resourceUri / _meta.ui.visibility.

Out of scope

Host <-> iframe JSON-RPC (ui/initialize, sandbox, postMessage).

Limitations

Roots (deprecated), sampling (deprecated), and experimental tasks are not currently implemented.

Synchronous elicitation is restricted to the stdio transport for the 2025 specs.

OpenTelemetry _meta propagation is currently not implemented. OAuth support does not include token acquisition, refresh, revocation, method-specific scope policies, or active-subscription revalidation.