Add remote MCP servers to Last9 AI
Register a remote MCP server for Last9 AI, test connectivity, and activate it for chat and investigations.
Connect Last9 AI to a remote MCP server your organization runs (for example an internal incident, payments, or ticketing MCP). After registration, tools from that server are available on the next activated chat turn. To make the assistant prefer those tools during investigations, bind them on a knowledge topic.
This page covers organization MCP management for Last9 AI. For connecting a coding agent to Last9’s hosted MCP, see Last9 MCP.
Prerequisites
- Last9 AI enabled for your organization. See Enable Last9 AI.
- An Admin user (writes and MCP diagnostics need an administrator).
- An API refresh token with write scope to create or update, and delete scope to remove servers. See Getting started with API.
- Your remote MCP endpoint reachable from Last9 over HTTPS, with any auth headers you will store on the server record. See Connection requirements and limits.
- Your organization slug (from the dashboard URL:
app.last9.io/v2/organizations/<org_slug>/...).
GET requests are available to organization users with an allowed read scope. Create, PATCH, and both diagnostic routes require an administrator and write scope; DELETE requires an administrator and delete scope. Both the user’s role and the token’s scope are checked. Agent tokens cannot access these management endpoints, including GET.
Register a remote MCP server
Worked example name: payments. Substitute your server name, URL, and credentials.
-
Mint an access token
Exchange a refresh token that includes write (and delete if you need cleanup):
curl -sS -X POST 'https://app.last9.io/api/v4/oauth/access_token' \-H 'Content-Type: application/json' \-d '{"refresh_token":"<refresh_token>"}'Export the access token:
export LAST9_TOKEN='<access_token>'export LAST9_ORG='<org_slug>'export LAST9_AI="https://app.last9.io/api/v4/organizations/${LAST9_ORG}/ai" -
Run a diagnostic on the remote MCP without saving
Confirm connectivity and tool discovery before you persist configuration. A failed diagnostic does not create a server.
curl -sS -X POST "${LAST9_AI}/mcp-servers/test" \-H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \-H 'Content-Type: application/json' \-d '{"name": "payments","transport": "streamablehttp","url": "https://mcp.example.com/mcp","headers": {"Authorization": "Bearer <mcp_credential>"}}'Use
transport: "sse"only if your server speaks SSE instead of Streamable HTTP. Fix URL or credentials until the diagnostic succeeds. Create and PATCH also run the same diagnostic and reject a failing candidate with422before saving it. A server that becomes unavailable after saving can still block later activation.You can also run the diagnostic on a saved server with
POST ${LAST9_AI}/mcp-servers/payments/test(send no body). -
Register the MCP server
curl -sS -X POST "${LAST9_AI}/mcp-servers" \-H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \-H 'Content-Type: application/json' \-d '{"name": "payments","transport": "streamablehttp","url": "https://mcp.example.com/mcp","headers": {"Authorization": "Bearer <mcp_credential>"}}'Expected:
201withactivation.status: "pending". Responses list header names and whether auth is configured — never secret values. Header values are stored as literal strings (they are not expanded from environment variables).Server
namemust match[a-z][a-z0-9_-]{0,99}(lowercase, start with a letter). The nametestis reserved becausePOST /mcp-servers/testis the diagnostic route. The URL must behttps://. -
List and read servers
curl -sS "${LAST9_AI}/mcp-servers" \-H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}"curl -sS -D - "${LAST9_AI}/mcp-servers/payments" \-H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" -o /tmp/mcp-payments.jsonKeep the quoted
ETagfrom the GET response for later PATCH or DELETE. Deployment-managed servers (such as Last9’s own MCP) appear in the list but cannot be mutated or tested through these routes. -
Activate with a chat turn
There is no separate activate endpoint. After create or PATCH,
activation.statusispending. Open AI Assistant and send a turn, then check:curl -sS "${LAST9_AI}/config/status" \-H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}"Expect
active. If activation fails (bad MCP URL or auth), new turns stay blocked until you fix or delete the server.
Server request fields
POST /mcp-servers and POST /mcp-servers/test accept the same JSON object. Only the following fields are allowed; unknown fields, duplicate JSON keys, and incorrect types are rejected.
| Field | Type | Required? | Meaning, defaults, and validation |
|---|---|---|---|
name | String | Yes | Stable identifier used in URLs, topic bindings, and mcp__{server}__{tool} names. Must match [a-z][a-z0-9_-]{0,99}: 1–100 characters, starts with a lowercase letter, then lowercase letters, digits, hyphens, or underscores. test is reserved. Cannot be changed with PATCH. |
transport | String | Yes; no default | "streamablehttp" for Streamable HTTP or "sse" for SSE. "http" is accepted as an alias and returned as "streamablehttp"; it does not permit an HTTP URL. "stdio" is not supported for API-created servers. |
url | String | Yes | Full HTTPS MCP endpoint, including its path and optional port. Maximum 4,096 characters; printable ASCII without spaces. Must include a hostname. Embedded username/password, query strings, fragments, backslashes, and percent escapes in the hostname/authority are rejected. Put credentials in headers, not the URL. |
headers | Object of string values | No; defaults to {} | Literal HTTP headers for requests to the remote server, for example {"Authorization":"Bearer ..."} or {"X-API-Key":"..."}. Maximum 64 entries; each value at most 8,192 printable ASCII characters. No environment-variable expansion. Header names must be valid HTTP token names and unique ignoring case. |
Header values cannot contain tabs, newlines, other control characters, or non-ASCII characters. Redaction placeholders such as [redacted], <redacted>, redacted, ***redacted***, or three or more asterisks are rejected; submit the actual credential when changing it. These transport-controlled headers are forbidden, ignoring case: Host, Content-Length, Transfer-Encoding, Connection, Upgrade, Accept-Encoding, Proxy-Authorization, Proxy-Connection, Keep-Alive, TE, and Trailer.
PATCH accepts only transport, url, and headers, with at least one field and a current If-Match header. Omitted fields keep their values. If supplied, headers replaces the entire header map rather than merging individual keys. Send all headers you want to retain; {} clears them and the resulting unauthenticated candidate must still pass the live diagnostic. null is not a clearing operation.
Minimal unauthenticated server
After exporting LAST9_AI and LAST9_TOKEN as above, this is the smallest create body:
curl --fail-with-body -sS -X POST "${LAST9_AI}/mcp-servers" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H 'Content-Type: application/json' \ --data '{"name":"payments","transport":"streamablehttp","url":"https://mcp.example.com/mcp"}'Replace the example endpoint with a working MCP server. Registration performs MCP initialization and tool discovery; an ordinary HTTPS API is not sufficient.
Connection requirements and limits
The endpoint must pass TLS certificate and hostname validation. Redirects are rejected: provide the final endpoint directly, including the correct trailing slash where required. Discovery responses must be uncompressed (Content-Encoding: identity or absent). The client requests identity encoding.
Destinations must resolve to public addresses by default. Private, loopback, link-local, multicast, and other non-public addresses are rejected unless the destination is explicitly permitted by your Last9 deployment. A private network endpoint being reachable from your laptop does not make it reachable from Last9. Contact support if you need a private destination.
Default discovery limits are 10 seconds, 10 tool-list pages, 1,000 tools, and 1 MiB of response bytes during initialization/discovery. These are connection limits, not request fields. JSON management requests are limited to 1 MiB. The total saved API-managed MCP configuration, including credentials, is also limited to 1 MiB; exceeding that saved-configuration limit returns 400 invalid_request.
Server responses
GET /mcp-servers returns {"servers": [...]}. GET /mcp-servers/{name} returns one server object and an ETag response header. Lists have no pagination parameters. Do not send query parameters on management routes.
| Server response field | Meaning |
|---|---|
name | Stable server identifier. |
transport | Normalized transport; an input of "http" is returned as "streamablehttp". Deployment-managed entries may use other transports such as "stdio". |
url | Saved endpoint. A deployment-managed non-HTTP server may have an empty URL. |
header_names | Sorted array of configured header names. Values are never returned. |
auth_configured | true when any headers are configured, even if those headers are not authentication headers. It is not proof that credentials work. |
managed_by | "api" for editable API-managed servers; "deployment" for protected servers. Deployment-managed entries cannot be created over, patched, deleted, or tested through the named diagnostic route. |
Create returns 201; PATCH and DELETE return 200. Successful writes wrap the resource:
{ "resource": { "name": "payments", "transport": "streamablehttp", "url": "https://mcp.example.com/mcp", "header_names": ["Authorization"], "auth_configured": true, "managed_by": "api" }, "saved": true, "activation": { "status": "pending" }}resource is the saved server, or {"name":"payments"} after deletion. saved: true confirms persistence; activation.status: "pending" means the configuration still needs activation. Create and PATCH return an ETag header for the new saved state. The ETag accounts for credential changes even though credentials are omitted from the response.
Diagnostic responses
Candidate test: POST /mcp-servers/test with the complete create-shaped JSON body. Saved test: POST /mcp-servers/{name}/test with no body (even {} is rejected). Neither requires If-Match; neither saves or activates configuration. The saved route uses stored credentials and accepts only API-managed servers.
A completed diagnostic normally returns HTTP 200, including when the diagnostic fails. Inspect the JSON fields rather than treating HTTP 200 as success:
{ "reachable": true, "authentication": "accepted", "protocol_valid": true, "tool_count": 3, "stage": "complete"}| Field | Meaning |
|---|---|
reachable | Whether an HTTP response was received. true alone does not prove MCP compatibility, accepted credentials, or a usable destination. |
authentication | "rejected" if the remote returned 401/403; "accepted" after a successful protocol test with configured headers; "not_required" after success without headers; otherwise "unknown". accepted does not independently establish what each header did. |
protocol_valid | Whether MCP initialization and bounded tool discovery completed successfully. |
tool_count | Number of tools in the completed discovery result. A successful zero-tool server is valid; a failed/incomplete discovery does not return a partial inventory. |
stage | Last stage reached: "destination", "initialize", "tools_list", or "complete". |
error_code | Included on failure, absent on success. Common values are listed below. |
The diagnostic returns a count, not tool names or schemas. Use your server’s advertised tool names when configuring topic bindings. Create/PATCH run the same diagnostic and return an HTTP 422 error instead of saving a candidate with rejected authentication or an invalid protocol. A successful diagnostic does not guarantee the server will remain reachable at activation or tool-call time.
Tool names and approval
| Call shape | Typical approval |
|---|---|
mcp__{server}__{tool} for an API-managed server | approve (human must confirm) |
{topic_id}__{tool} from a knowledge topic mcp binding | As configured on the topic (auto or approve) |
Deployment-managed Last9 tools (mcp__last9__…) | Per Last9 product policy |
Creating an MCP server alone does not make the model prefer those tools. Register the server here, then bind tools on a knowledge topic with use: ["incident_triage"] and put the procedure in the topic overview. Prefer topic-prefixed tools in runbooks so auto approval applies where you configured it.
Update or remove
Read the server first and keep the quoted ETag. Send it as If-Match on PATCH or DELETE. Missing If-Match returns 428; a stale value returns 412.
curl -sS -X PATCH "${LAST9_AI}/mcp-servers/payments" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H 'Content-Type: application/json' \ -H 'If-Match: "<etag_from_get>"' \ -d '{"url":"https://mcp.example.com/mcp/v2"}'If PATCH validation fails, the previous saved configuration remains unchanged. To rotate one credential while keeping other headers, include the full desired map. For example:
curl --fail-with-body -sS -X PATCH "${LAST9_AI}/mcp-servers/payments" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H 'Content-Type: application/json' \ -H 'If-Match: "<etag_from_latest_get>"' \ --data '{"headers":{"Authorization":"Bearer <new_mcp_credential>","X-Team":"payments"}}'The ETag must match the exact current quoted value. Do not use *, weak ETags, or a list of tags. After every successful edit, keep the new response ETag or GET again. After a stale ETag error, re-read and review the latest state before you retry. Do not bypass the ETag check.
Delete (needs delete scope on the token):
curl -sS -X DELETE "${LAST9_AI}/mcp-servers/payments" \ -H "X-LAST9-API-TOKEN: Bearer ${LAST9_TOKEN}" \ -H 'If-Match: "<etag>"'DELETE must have an empty body. It does not run a diagnostic, so you can remove a server that has become unreachable. Update any topic bindings that depend on it before deleting it.
Activation status
There is no activate API. Saving a create, PATCH, or DELETE marks the shared knowledge/MCP configuration pending; the next AI turn attempts activation. Reading status and running diagnostics do not activate it. Already-running turns retain their admitted configuration.
GET /config/status field | Meaning |
|---|---|
status | "pending" after saving, "active" after successful activation, or "error" if activation fails. |
last_attempt_at | ISO 8601 UTC timestamp of the most recent activation attempt; null before any attempt. A new save can retain the previous attempt timestamp. |
request_id | Correlation value for the latest mutation, or null when unavailable; not a resource version. |
error_code | Present after failure as "configuration_unavailable"; cleared by a new save or successful activation. |
An activation error blocks new turns rather than silently using stale settings. Fix or delete the problematic server, then send another turn and recheck status. A server can pass registration but later fail because its endpoint, credentials, or available tools changed. active proves configuration activation; verify actual tool behavior in a chat too.
Checklist
- Admin has AI enabled and an API refresh token with write and delete scopes.
POST /mcp-servers/testsucceeds against the real HTTPS MCP URL and credentials.POST /mcp-serversreturns201and the server appears inGET /mcp-servers.- After one AI Assistant turn,
GET /config/statusisactive. - A test chat can request
mcp__payments__…(expects approval) or a topic-bound{topic}__…tool. - Servers that become unreachable after they are saved are fixed or deleted, so they cannot block activation.
- For investigations, continue with knowledge topics to bind tools and upload runbooks.
Related
- Knowledge topics — runbooks,
incident_triage, and MCP tool bindings - AI Assistant — run a verification chat after activation
- Last9 MCP — hosted Last9 tools for coding agents
- Getting started with API — refresh and access tokens
Troubleshooting
Management errors return a JSON body of the form {"error":"code"}. A diagnostic can instead return HTTP 200 with protocol_valid: false and an error_code.
| Symptom or error | Fix |
|---|---|
400 invalid_request | Check required fields, unknown or duplicate keys, the name format (lowercase; cannot be test), the transport, the HTTPS URL restrictions, reserved headers, and header value limits. PATCH must be nonempty and cannot contain name. Correct the field and send the request again. |
403 on DELETE | Token has write but not delete scope. Create a refresh token that includes delete, or ask an Admin. |
404 not_found | Check the server name and the organization slug. |
409 resource_conflict | The name already exists, or the server is deployment-managed and protected. Choose a new name, or PATCH the existing API-managed server. |
412 / 428 on PATCH or DELETE | Send a fresh If-Match from the latest GET. |
413 request_too_large | The JSON body exceeds 1 MiB. Reduce the request size, for example by removing unneeded headers. |
422 mcp_authentication_rejected on save | Remote returned 401/403; provide valid literal credentials. |
mcp_destination_rejected | Public destination/DNS rules, redirects, cross-origin endpoints, or compressed responses. Use a direct allowed HTTPS endpoint with uncompressed responses. |
mcp_deadline_exceeded | Initialization/discovery did not finish in the connection deadline. Check endpoint responsiveness and pagination. |
mcp_discovery_limit | Too many tool pages/tools or excessive discovery response bytes. Reduce the advertised inventory/schema size. |
mcp_tool_collision | The server advertises duplicate tool names. Give each tool a unique name on the remote server. During activation, topic-prefixed name collisions can instead surface as configuration_unavailable; bind the colliding tools on separate topics. |
mcp_connection_failed | Check DNS, TLS, the endpoint path, the transport, headers auth, and MCP protocol support; inspect remote server logs. Run the diagnostic again after each fix. |
503 persistence_uncertain | A write may have persisted. GET and compare the current resource before retrying, using its fresh ETag. |
503 persistence_unavailable or upstream_unavailable | Read back state and retry later; contact support if persistent. |
| Config stuck / new turns rejected | Run GET /config/status. Fix or delete the failing MCP server, then send another chat turn. |
| Model never calls your MCP | Server registered but no knowledge binding or overview that names the tools. Bind the tools on a topic with incident_triage and name them in the overview. |
| Auto approval does not apply | You called mcp__{server}__{tool} instead of {topic_id}__{tool}. Bind the tool on a knowledge topic. |
Please get in touch with us on Discord or Email if you have any questions.