{# One registered server: what it is, what can be changed, and every operation it has (spec §7.1). Two forms with nothing between them but a heading, and both of them are one button and one write. Every field on the settings form changes how the whole server behaves, so nothing on it posts as you type; the table below is saved the same way, by the button underneath it, which is how the picker an operator registered the server on already worked (task 114). The settings are also two things: a card that says what the server is set to, and, at ``?edit=1``, the same card as that form. An operator who came to read which base URL a server proxies to should not be one keystroke away from changing it (task 113). Both modes are rendered here rather than toggled by script, because a card whose only way in is a listener is a card that stops existing when the script does. Neither credential is ever rendered. What is shown is ``Set`` or ``Not set`` and a box to tick; with the box unticked the route does not read the credential fields at all (spec §7.3). #} {% extends "base.html" %} {% from "partials/field.html" import field, choice, switch %} {% from "partials/credential_fields.html" import credential_fields %} {% from "partials/status_badge.html" import status_badge %} {% from "partials/tool_counts.html" import tool_counts %} {% from "partials/toggle_button.html" import toggle_button %} {% block title %}{{ settings.server.name }} · mcp-api-gateway{% endblock %} {% block scripts %} {% endblock %} {% block content %} {# **Needs attention** is not shown here. It lives on the review strip above the operation table, inside the region htmx swaps, so that settling the last row takes the badge off the page it was settled on rather than at the next reload. A badge in this toolbar would be a second copy that could disagree. #}

{{ settings.server.name }}

{{ status_badge("enabled" if settings.server.enabled else "disabled") }} {# A whole-page action, deliberately: a refresh moves the summary below, every row's status, the counts and the flag at once (spec §5.4). Absent on the gateway's own server, which has no document to re-read (task 102). #} {% if settings.editable %}
{% endif %} {# The switch, where the operator is already looking (task 112). The same partial the list's Actions column uses, so the two buttons cannot come to read differently, and with no ``target``: switching a server moves the badge above, the active count below and the note under this bar at once, which is the argument Refresh Spec makes on this page for answering with a page rather than a swap. It follows Refresh Spec and precedes All servers because these two act on this server and that one leaves it. Offered for the built-in server too, where it is the only thing anybody decides. #} {{ toggle_button(overview) }} All servers
{# Why the server is off, beside the button that turns it back on. This is the only place the page says that the gateway itself took it out of service, and it used to be the hint under a switch that is no longer here (task 112). #} {% if settings.enabled_note %}

{{ settings.enabled_note }}

{% endif %} {% if settings.alerts %} {% endif %}
Spec URL
{{ settings.server.spec_url }}
Format
{{ settings.server.spec_format }}
Last spec download
{{ status_badge(overview.state, label=overview.refreshed, title=overview.refresh_title) }}
{# The list's Status cell, restated: an operator arrives here from that table and should not have to re-learn the numbers they just read. Whether the server is on is said by the badge in the toolbar above, which is why the green number can be a bare 0 here as it is there (task 106). #}
Status
{{ tool_counts(overview.counts) }}
{# Nothing on the gateway's own server is an operator's to change, now that the one thing that was — the switch — is the button in the toolbar above (task 112). So the card is the sentence saying so and nothing else: a box still shaped like the form the other servers get, in the place that form would be, rather than a heading that quietly vanishes on this one page. The rule underneath is in the repository, which refuses a patch of anything but ``enabled`` however it arrives (task 102). #} {% if not settings.editable %}

Settings

{{ settings.uneditable_note }}

{% elif not settings.editing %} {# The card as text, which is how the page opens. Every value the form below can change, in the order the form asks for it, read off the stored row and never off ``fields`` — a refused submission is still in there, and a view built from it would be describing a save that did not happen (task 113). Not here: whether the server is on. That is the badge and the button in the toolbar above, and one fact gets one control and one statement of itself (task 112). The two credential lines are lifted out of the form unchanged, because they were never anything but a view in the first place. #}

Settings

{# A link, so it works with no script, and one that lands on this card rather than at the top of a page two hundred operations long. It carries the table's filter with it, which is the whole reason the mode is a query parameter rather than a path of its own. #} Edit
Display name
{{ settings.server.name }}
Tool prefix
{{ settings.server.tool_prefix }}
Base URL
{{ settings.server.base_url }}
Refresh automatically
{{ "Yes" if settings.server.auto_refresh else "No" }}
Rate limit
{{ settings.rate_limit_note }}
API authentication
{{ settings.credential.label }} {% if settings.credential.auth_type %} · {{ settings.credential.auth_type }} {% endif %} {{ settings.credential.note }}
Spec download
{{ mode_labels[settings.server.spec_auth_mode] }}
Spec credential
{{ settings.spec_credential.label }} {% if settings.spec_credential.auth_type %} · {{ settings.spec_credential.auth_type }} {% endif %}
{% else %} {# The same card, open. Everything that asks the gateway a question while it is being typed — the rename preview, the two reveal panels — is inside this branch and nowhere else: a target left on a read-only page is a hole nothing aims at (task 113). #}

Settings

{{ field("name", "Display name", value=settings.fields.name, required=true, hint="What this server is called on every page and in every message.", error=settings.errors.get("name")) }} {# Written out rather than passed through the field macro: it is the one box on this form that asks the gateway a question while it is being typed. #} {% include "partials/rename_preview.html" %} {{ field("base_url", "Base URL", value=settings.fields.base_url, required=true, hint="Where every tool call this server exposes goes.", error=settings.errors.get("base_url")) }} {# No Enabled switch here. It is the button in the toolbar, which writes on the press; a copy on this form would write again on Save, and pressing Enable and then saving would turn the server straight back off (task 112). #} {{ switch("auto_refresh", "Refresh automatically", checked=settings.auto_refresh, hint="Re-read the spec on the gateway's schedule. Changes are flagged, never applied.") }} {# Two boxes and one setting: both empty is no limit. The note above them describes the stored row rather than what is in the boxes, so a rejected form does not claim a cap that was never saved (task 101). #}
Rate limit

{{ settings.rate_limit_note }}

{{ field("rate_limit_calls", "Calls", value=settings.fields.rate_limit_calls, type="number", placeholder="no limit", hint="How many calls this server will take in the window below.", error=settings.errors.get("rate_limit_calls")) }} {{ field("rate_limit_seconds", "Seconds", value=settings.fields.rate_limit_seconds, type="number", placeholder="no limit", hint="How long that window is. Leave both boxes empty for no limit; a change applies to the next call.", error=settings.errors.get("rate_limit_seconds")) }}
API authentication

How the gateway authenticates the tool calls it proxies.

Credential
{{ settings.credential.label }} {% if settings.credential.auth_type %} · {{ settings.credential.auth_type }} {% endif %} {{ settings.credential.note }}
{{ switch("replace_credential", "Replace the API credential", checked=settings.replacing_credential, reveal="api_replace", hint="Leave this alone and the stored credential is not touched.") }}
{{ choice("auth_type", "Type", auth_options, settings.fields.auth_type, reveal="api", error=settings.errors.get("auth_type")) }} {{ credential_fields("api", errors=settings.errors) }}
Spec download

How the document itself is fetched on every refresh.

Mode
{{ mode_labels[settings.server.spec_auth_mode] }}
Credential
{{ settings.spec_credential.label }} {% if settings.spec_credential.auth_type %} · {{ settings.spec_credential.auth_type }} {% endif %}
{{ switch("replace_spec_credential", "Change how the spec is downloaded", checked=settings.replacing_spec_credential, reveal="spec_replace", hint="Leave this alone and the stored spec credential is not touched.") }}
{{ choice("spec_auth_mode", "Authentication", mode_options, settings.fields.spec_auth_mode, reveal="spec_mode", error=settings.errors.get("spec_auth_mode")) }}
{{ choice("spec_auth_type", "Type", spec_auth_options, settings.fields.spec_auth_type, reveal="spec_type", error=settings.errors.get("spec_auth_type")) }} {{ credential_fields("spec_type", prefix="spec_", errors=settings.errors) }}

A stored credential is never shown again, so the boxes above come back empty after a correction. Fill in the ones the type you chose needs.

{# Cancel comes back to this page's own view mode and abandons only what was typed. It used to leave for the list, which was the only destination it could have had when there was nothing here to come back to; leaving the page is still All servers in the toolbar (task 113). Save rather than Save settings, because the heading above says what is being saved. #}
Cancel
{% endif %} {# In the outline and nowhere else. The bar this sat in held one word and no actions, and the table under it, its count line and the Save below it all say it louder; but a page whose second half is a two-hundred-row table needs a heading to be navigable by heading, and the word itself is a decision — these are Tools, not Operations (tasks 103 and 120). #}

Tools

{# Said once here rather than on every row, which is where it would be noise. #}

A name box left empty is the generated name, shown in grey after the prefix this server publishes under. Nothing here is written until Save at the bottom is pressed, and then all of it is — including the rows a filter is hiding.

{% if operations.rows %} {# A GET form: filtering reads and nothing else, and the filter it produces is in the URL, which is what every row's Save carries back with it. #}
{% endif %} {% if operations.rows %} {# The form every control in the table belongs to, with nothing inside it. The rows bind to it by id (``form="operations-form"``), which is what lets one form hold controls spread across two hundred ```` that each also contain forms of their own. Out here beside the filters rather than inside the region htmx replaces, so that filtering the table or settling a row leaves the form the operator is part-way through filling in exactly where it was (task 114). No ``hx-post``: this button writes more of the page than the table. The counts above it, every effective name and the tool counts in the summary at the top all move at once, and the sentence saying how much of that happened has to land somewhere — which is the argument the settings form above makes for answering with a page rather than a swap. #}
{% endif %} {# Included whether or not there are rows, unlike the filters above it. It is the region htmx swaps, and retiring the last ``removed`` operation has to land somewhere: a target that stopped existing when it emptied would leave the operator's click with nowhere to go. The table says so itself when it has nothing in it. #} {% include "partials/operation_table.html" %} {% if operations.rows %} {# Named rather than a bare Save, in the shape the picker ends with: by the time an operator reaches this button the heading is two hundred rows above it, and the settings card up there has a Save of its own. The count line above the table is what says how much is about to be written. #}
{% endif %} {% endblock %}