Adding a new Plugin
A plugin is either a remote MCP server, which 2501 connects to by its URL, or a hosted MCP server, which the engine runs itself from a container image. Open Plugins and select Add MCP. Paste the server’s MCP URL, its README configuration or its command line: the form asks only for what it could not find there. Check the authentication 2501 detected (Bearer token, API key or OAuth); for a server that answers without authentication, select Change to pick another or leave Anonymous / Agent managed selected. Enter the name and description, choose the scope and enabled state in the top bar, and save. API-key authentication also needs a header name such asX-Api-Key. For Bearer token and API key, choose the Secret or Vault path credential the plugin sends: every agent assigned the plugin uses it. Add MCP waits while the field is being read, with the detection status inside the URL input, and stays unavailable while its text cannot be read, so a paste that fails never saves the previous server. The plugin starts enabled, and reaches no agent until it is assigned. Saving it does not deploy or connect to a service: assign it to an agent and check the connection from that agent. A hosted MCP server starts when a task or a check first uses it.
A hosted MCP server runs from a container image: paste its docker run command in the MCP URL field, and the hosted MCP server’s fields appear below it, to check or complete: the image, such as registry.example.com/team/mcp-server:1.2, its arguments one per line, and its environment variables. A variable holds either a plain value, which everyone who can read plugins sees, or a Secret or Vault path credential, which 2501 reads when the hosted MCP server starts and never shows. The engine runs one container per hosted MCP server and organization next to itself, shared by that organization’s tasks, and stops it after thirty minutes without use or when the engine stops; a hosted MCP server in use has no time limit. Within a minute of a plugin being deleted or turned off, its hosted MCP server stops in every organization, even during a call, which then fails. The same goes for a container its plugin no longer describes: a plugin switched to a remote MCP server, moved to another organization, or saved with another image, other arguments, other variable names or other credentials. A hosted MCP server whose image, arguments or variables change, a rotated secret included, starts in a new container. After a rotated secret or a changed plain value, the old one stops once the new one runs and no call still uses it; after any other change, it stops within a minute, even during a call. A container gets at most half a CPU and 512 MiB of memory; on Kubernetes its pod requests a quarter of a CPU, and 64 MiB of ephemeral storage with a limit of 256 MiB, past which Kubernetes evicts it. On Docker its /tmp holds at most 64 MiB, taken from its memory; the rest of its file system stays writable and is not bounded. A hosted MCP server that fails to start is tried again after 5 seconds, then after longer waits up to 5 minutes. A connection check in that time shows when the next try is and why the last one failed, including the hosted MCP server’s last error output when it exited during startup; a task is told only that the plugin could not be reached, and the engine logs the full error (MCP plugin is unavailable). The engine replaces the values of the hosted MCP server’s credential variables with [REDACTED] in that output and in its logs. A hosted MCP server must answer the MCP handshake within 60 seconds of starting; one that does not is started once more in a fresh container, then fails. Set MCP_HOSTED_HANDSHAKE_TIMEOUT_MS on the engine for an image that needs longer to start. On Kubernetes a hosted MCP server’s pods carry the label app.kubernetes.io/managed-by: 2501-mcp-hosted, and the mcp-hosted-isolation network policy the CLI generates with the other manifests holds those pods, and only those, to DNS and public addresses, with no traffic in. To let them reach an internal service, such as a vCenter, apply a policy of your own that selects the same label and allows its range; allow-mcp-hosted-internal in kubernetes-overlays/networkpolicy.example.yml is one to adapt. The pods get no Service environment variables. On Amazon EKS the policy applies a few seconds after a pod starts unless the VPC CNI runs with NETWORK_POLICY_ENFORCING_MODE=strict, which denies every new pod all traffic until its policies apply. On Docker a hosted MCP server runs on a network of its own, 2501-mcp-hosted, and cannot reach other hosted MCP servers or containers that publish no port, but it reaches every container on the machine that publishes a port, 2501’s database included when its port is published, even on 127.0.0.1 only, and whatever the machine reaches: your internal network, a cloud metadata address such as 169.254.169.254, and the machine’s own services; the firewall rules in Isolate hosted MCP servers on Docker, required on Docker, close those. On Kubernetes a hosted MCP server’s variables reach its pod through a Secret the pod owns and that is deleted with it, so the engine’s role needs to create secrets. Only tenant-level administrators can add, change or delete a hosted MCP server; an organization administrator can switch their organization’s running hosted MCP server off, in a save that changes nothing else. In the plugin’s details, that administrator gets a Turn off button, and everything else stays read-only; once it is off, the details say that only a tenant-level administrator can change it or turn it back on, and no Delete is offered. An existing plugin switches between a remote MCP server and a hosted MCP server only in a save that also disables it. Its first start may pull the image, so check the connection from an agent once after saving. If a proxy in front of Command Center gives up on the check first, the agent’s form says the server is still starting: check again a minute later. A hosted MCP server never runs as root, whatever user its image names: it runs as user 65532, or on OpenShift as the user the cluster assigns. The image must therefore work as any non-root user: its files readable by every user (readable by group 0, as OpenShift expects), and nothing written outside /tmp, which is its HOME unless it sets one. Serve an image that cannot through an HTTP wrapper instead.
The MCP URL field also takes what a README hands out: an mcpServers entry (Claude, Cursor, Windsurf), a servers entry (VS Code), comments included, or a command line such as claude mcp add or docker run. There is nothing to press: half a second after the text stops changing, the engine connects to the URL the way a task would and lists its tools without calling any. It tries Streamable HTTP first, with the 2026-07-28 protocol and then the 2025 versions, and falls back to HTTP+SSE for an older server. The form then fills in the name, URL, transport and authentication, and shows a verdict followed by what to do. The name is the one a pasted configuration gives, else the name the server announces for itself, else one taken from the URL’s host name (none for an IP address or localhost), without words such as mcp and server; change it before saving if it does not suit. The verdict says how many tools it verified, or why it could not connect, such as Not an MCP endpoint (HTTP 404) or, when no HTTP answer came, what the network said: a refused connection, a host name that does not resolve, a TLS certificate the engine does not trust or a failed TLS handshake, or a timeout. A pasted configuration leaves only its URL in the field, and Detect again repeats the check. Add MCP stays unavailable while the engine refuses the address, for example cleartext HTTP to a public address; a server it could not reach can still be added. An authentication you change while the check runs is kept. A configuration that sends an Authorization: Bearer header is set up for a Bearer token, and any other header for an API key in that header. A server that refuses anonymous access and names its OAuth provider is set up for OAuth: 2501 registers itself when the provider allows it, and otherwise the form asks only for the client ID of a client registered there. A server that refuses anonymous access without naming a provider is set up for a Bearer token. Only the URL is sent: headers, tokens and other values in a pasted configuration are neither sent nor saved. A pasted docker run command, in a configuration or on its own, fills in the hosted MCP server instead: its image, its arguments, and its -e variables. A variable whose name holds a secret word, such as GITHUB_TOKEN, API_KEY, PGPASSWORD, SSH_PASSPHRASE or accessToken, becomes a credential to choose, and its pasted value is not kept; a name where that word is only part of another, such as BASE_PATH or MAX_TOKENS, stays a plain value. Options 2501 does not grant a hosted MCP server, such as host folders (-v), published ports (-p) or network settings, are left out and named. A pasted npx or uvx configuration is not run: build an image for it, as described below. An mcp-remote bridge is replaced by the URL it points to.
Without detection, connections created here use Streamable HTTP. Through the API, a connection can select either transport, and editing it here keeps that transport.
Recommended Kubernetes MCP
For Kubernetes, we recommend containers/kubernetes-mcp-server, deployed in your cluster and added by its URL, such ashttp://kubernetes-mcp.<namespace>.svc:8080/mcp. Pin its image to a version you have tested, for example quay.io/containers/kubernetes_mcp_server:v0.0.67, rather than a moving tag. Run it read-only (read_only = true in its configuration file) and deny Secrets (denied_resources), and give its service account a Role that grants only get, list and watch, on the resources and in the namespaces your agents need to read. An installation that lets agents change the cluster through it turns read_only off, and must still deny Secrets: keep denied_resources = [{ group = "", version = "v1", kind = "Secret" }] in its configuration file (the setting as of v0.0.67), and leave secrets out of its service account’s Role. Whatever a tool returns, a Secret’s values included, goes into the task’s trace, is stored with the task and is sent to the model provider. This server has no sign-in of its own in that setup, so keep its Service reachable only inside the cluster: the engine reaches an internal address over plain HTTP without any setting.
Run hosted MCP servers without internet access
Put the hosted MCP server’s image in a registry your cluster, or the engine’s machine on Docker, can pull from, and enter it by that reference. On Kubernetes, a hosted MCP server’s pod names the pull secrets that the engine settingMCP_HOSTED_IMAGE_PULL_SECRETS lists, comma-separated. Unset, it names the executors’ EXECUTOR_MCP_IMAGE_PULL_SECRET, which on a CLI installation is the short-lived login to 2501’s own registry, where your images are not. Set to an empty value, it names none, so the pod takes the pull secrets of the namespace’s default service account, such as OpenShift’s login to its internal registry. On Docker, the engine pulls an image the machine lacks with the login docker login stored for that registry in the engine’s Docker configuration: config.json in the directory DOCKER_CONFIG names, or ~/.docker, inside the engine’s container, so mount the file into it and point DOCKER_CONFIG at its directory. Credential helpers are not read. Otherwise pull or load the image on the engine’s machine first (docker pull, or docker load from a saved image): the engine uses an image the machine already has. On the Swarm stack the CLI renders, name that file in the install configuration (swarm.docker_config) instead of mounting it yourself: see Hosted MCP servers on Docker Swarm.
A server published only on npm or PyPI needs an image built for it. Build it in your own pipeline, through your package mirror, and pin the version so that an installation does not change on its own:
python:3.12-slim, pip install the pinned package and set its command as the ENTRYPOINT. A server that only reaches an online service, such as Context7, still needs that service: without internet access, choose servers that talk to your own systems.
Isolate hosted MCP servers on Docker
On Docker, including Docker Swarm, these firewall rules are a required step on every machine where the engine runs hosted MCP servers. Without them a hosted MCP server’s network keeps it away only from containers that publish no port. It still reaches every container that publishes a port, on that container’s own address and published port, even a port published on127.0.0.1 only, such as a database, and whatever the machine itself reaches: private ranges, the cloud metadata address and the machine’s own services. To hold hosted MCP servers to public addresses, drop their traffic to private ranges, the metadata address and the machine itself, as root on the engine’s machine, once the engine has started a first hosted MCP server and created the network:
DOCKER-USER covers traffic to other machines and to other containers, published ports included, since Docker gives containers addresses in private ranges, and INPUT covers the machine’s own services. If the Docker daemon gives networks addresses outside these ranges (default-address-pools), add those ranges to the loop. Name resolution keeps working, since Docker resolves names for the container from the machine itself. To let hosted MCP servers reach an internal service, insert (-I) an ACCEPT rule for its address after running these, so it comes first. The rules last until the machine restarts, and apply to the network as it is: save them with your firewall tooling, and run them again if the network is removed and created again. On Kubernetes, the network policy does this instead.
What bounds a hosted MCP server
Only a tenant-level administrator adds or changes a hosted MCP server, and 2501 runs its image exactly as named, so trusting a tenant administrator with hosted MCP servers means trusting any image they name. The container runs without root, gets no Linux capabilities and cannot gain privileges. It has at most half a CPU and 512 MiB of memory, on Kubernetes at most 256 MiB of writable storage, and on Docker at most 256 processes and a 64 MiB/tmp. On Kubernetes, the network policy the CLI applies lets it reach only DNS and public addresses; on Docker, the rules above do the same, and without them it reaches other containers’ published ports and the machine’s network.
Managing Plugins
Only administrators can create, edit (enabling and disabling included) and delete plugins: an organization administrator manages the organization’s plugins, except hosted MCP servers, which it can only switch off, and a tenant-level administrator also manages tenant-wide ones. Other users see the plugin list and each plugin’s details, read-only, and so does an organization administrator for a tenant-wide plugin. Go to Command Center → Plugins. The table shows each plugin’s description, scope, state and creation time. Open a row or select its arrow to reach the plugin’s page; the trash action deletes an unassigned plugin. The plugin page keeps scope, enabled state and Save Changes in its top bar. Its name, description, connection and authentication fields appear in one form.Scope
The Scope selector controls which organizations can use the plugin. Scoped to Organization, the default, associates it with the currently selected organization only, restricting it to that org’s agents. Scoped to Tenant makes it available to all organizations in the tenant. Selecting the tenant scope requires tenant-level access; without it the selector stays on Scoped to Organization. Changing an OAuth plugin’s scope disconnects its OAuth accounts and forgets its trusted provider. While an enabled OAuth plugin is assigned to agents, the scope change is refused unless the same save turns the plugin off.Enabled Status
Allow to quickly enable or disable a plugin. Useful if you realize some MCP functions do not behave in the way you expected. If a plugin is disabled, the agent can’t call any of its functions, even if the plugins are associated to an agent.Assign to agents
A saved plugin’s page lists the agents that have it. Add an agent appears before compact assignment chips and searches the agents of the plugin’s organization, or of the selected organization for a tenant-wide plugin. Select the X on a chip to remove a direct assignment. Each change saves on its own and is the same assignment as choosing the plugin on the agent’s page. Anyone who can edit agents can do this, even on a plugin they cannot edit. Agents that inherit the plugin from their specialty are listed too, read-only, with the specialty’s name: change them from the specialty. Archived agents are not listed.Assign to a specialty
A saved plugin can also be given to a specialty. Add a specialty appears before compact assignment chips and searches for another; select a chip’s X to remove the assignment. Each change saves on its own. The same choice is on the specialty’s page: MCP servers lists the plugins it hands out and searches for another one it can take. Every agent of that specialty then has the plugin, including agents created later, and an agent that changes specialty gains or loses it. An agent’s own assignment takes precedence. An inherited plugin cannot be taken away from one agent alone; detach it from the specialty instead.DELETE /api/v1/agents/{agentId}/plugins/{pluginId} on an inherited plugin returns 409 and names the specialty in details.specialty_id.
An organization’s specialty takes that organization’s plugins and tenant-wide ones, and a tenant-wide specialty only takes tenant-wide plugins, so an organization plugin’s editor never offers a tenant-wide specialty. Attaching a plugin to a tenant-wide specialty or detaching it requires tenant-level access; without it, the editor offers none and shows their switches disabled. Assigning an enabled OAuth plugin is refused while an organization the specialty reaches has no connected account. For the same reason, creating an agent with a specialty that hands out an enabled OAuth plugin, or switching an agent to it, is refused until the agent’s organization has connected its account. A plugin a specialty holds cannot be deleted, or moved to an organization the specialty does not belong to, until it is detached, and the specialty cannot move to a scope that no longer takes the plugin. The refused move answers 409 CONFLICT with details.reason plugin_move_blocked and the specialties in details.specialty_ids, the same answer as a move refused for its agents, which lists them in details.agent_ids.
Plugins per Agent
You can have granular control over which agent has access to a certain range of plugins. Navigate to Agents and select the agent you want to manage. From here, you can add or remove plugins for this agent. Anyone who can edit the agent can do this; it does not require an administrator. Each assignment shows the plugin kind and authentication method. When you create an agent in Command Center, the enabled plugins that need no account (anonymous plugins, hosted MCP servers, and Bearer or API-key plugins holding their credential) are added by default, except the ones its specialty already hands out, which show as inherited. This default belongs to the form: an agent created through the public API, a CSV or JSON import or the AI Assistant gets only the plugins the request names, plus the ones its specialty hands out.Considerations
Plugins and MCP functions are loaded in agent’s context, which can impact an agent performance overtime (especially smaller models), so don’t forget to clear your agent memory every once in a while to avoid hallucination, especially with plugins exceeding a few hundreds commands.Public API
Use/api/v1/plugins with a session or a scoped API key to list and create plugins. Read, update or delete one through /api/v1/plugins/{id}: updates use POST and preserve fields you omit. Search uses GET /api/v1/plugins/search, and list/search responses use cursor pagination. tenant_wide=true keeps only the tenant-wide plugins, tenant_wide=false only the ones an organization owns.
GET /api/v1/plugins/{id}/agents lists the agents that have a plugin, each with assignment_source (agent or specialty) and, for an inherited one, the specialty. POST /api/v1/plugins/{id}/agents/{agentId} assigns it to one agent and DELETE removes that assignment: the same writes, checks and errors as POST /api/v1/agents/{agentId}/plugins and DELETE /api/v1/agents/{agentId}/plugins/{pluginId}. GET, POST and DELETE /api/v1/plugins/{id}/specialties[/{specialtyId}] do the same for specialties.
Organization-scoped keys can manage their own plugins and read shared tenant plugins. They cannot change tenant-wide plugins or reach another organization’s records. Creating a plugin without org_id makes it tenant-wide, which needs tenant-level access: an organization-scoped key or an organization administrator gets 403 and sends its organization’s org_id instead.
Without a URL connection, creation accepts GitHub MCP Plugin, Notion MCP Plugin and vmware, and saves the plugin enabled unless the request sends enabled: false, like any new plugin. Records without an endpoint report Migration required and provide no tools. Creation compatibility does not start a built-in backend. Other existing legacy names remain readable and editable; creating a new custom name requires a URL connection.
Remove all agent and specialty assignments before deleting a plugin. Deleting an assigned plugin returns 409 CONFLICT and preserves both the plugin and its assignments; the message names the assigned agents and specialties, up to five of each, details.agent_ids lists the agents’ IDs and details.specialty_ids the specialties’. Archiving an agent removes its plugin assignments. Moving a plugin to an organization returns 409 CONFLICT while agents outside it are still assigned. Assigning an organization plugin to another organization returns a client error. The plugin 409 CONFLICT answers carry details.reason: plugin_in_use, plugin_move_blocked, plugin_name_conflict (with field: "name") or plugin_inherited.
Creation and renaming preserve the duplicate-name rule: organization plugins cannot repeat a name in the same organization; a tenant-wide plugin rejects a name already visible anywhere in the tenant. Moving a plugin to another organization, or making it tenant-wide, follows the same rule and returns 409 CONFLICT with plugin_name_conflict when the name is taken there. Two requests saving the same name at the same time cannot both pass: the second waits for the first and is checked against it. Names are compared ignoring case, so QA-ECHO is taken once qa-echo exists, since both would lead the same tool names; plugins that already share a name this way keep working and keep their names. A name cannot hold control characters such as a line break.
Connect an MCP endpoint
Create a plugin through the public API with a custom name, an optional description, and an anonymous connection:enabled starts enabled. Set enabled: false when creating or updating a plugin to keep its assigned agents from using it, and enabled: true to let them again, from the next turn of a task already running. A Bearer or API-key plugin is enabled only with its credential: creating one without credential_id takes enabled: false, and returns 400 otherwise. An OAuth plugin can be created enabled, and then takes an assignment only in organizations that connected their account. The Plugins page saves a new plugin enabled, and asks for the credential of a Bearer or API-key plugin before saving it. Saving a URL never connects to it, resolves a credential or deploys a server. The API also accepts authentication: { "method": "bearer", "credential_id": "<credential ID>" } or authentication: { "method": "api-key", "header_name": "X-API-Key", "credential_id": "<credential ID>" }. These settings name the credential, never its value. API-key header names must be valid HTTP names and cannot replace transport or session headers such as Host, Content-Type, Mcp-Session-Id or Mcp-Protocol-Version. Custom headers such as X-API-Key and raw Authorization schemes are accepted. A Bearer or API-key plugin saved without credential_id is a draft that cannot be enabled until it has one. An assignment can be checked before enablement. OAuth uses the separate authorization flow below.
Updates can change the name or description, or replace the entire connection object. Omitting a field preserves it. A plugin without an endpoint can be given one in an update that also leaves it disabled; see Give a legacy plugin an endpoint. No other change of connection type is accepted.
URLs cannot contain a username, password, fragment or literal metadata/link-local address. Use HTTPS for external endpoints. Internal HTTP works without configuration: the engine permits cleartext to addresses inside its own network, whatever their port or host name, so a new internal MCP needs no deployment change. No setting turns this off: the engine always allows cleartext HTTP to private addresses. The decision is made on the address a name resolves to, so a public address is refused even when the URL looks internal. Storing an HTTP URL grants no permission by itself. When the engine refuses an endpoint by these rules, a connection check and the task’s connection history name the rule rather than a connection failure: cleartext_not_allowed for cleartext HTTP to an address outside its network, destination_not_allowed for a metadata or link-local address, and redirect_not_followed for a server that answers with a redirect, which the engine never follows. An engine that sends its outbound traffic through a proxy (HTTP_PROXY, HTTPS_PROXY) sends MCP endpoints and OAuth providers through it too, except the hosts NO_PROXY names, so list internal MCP services there. Behind the proxy, the engine still resolves each destination and applies these rules before the request, so it must be able to resolve those names itself; the proxy then resolves the name again on its own, and its own rules decide if the answer changed in between. A hosted MCP server reaches no proxy unless its variables name one, such as HTTPS_PROXY.
If endpoint restrictions change, previously stored settings remain readable and deletable. You can still edit the description or keep the plugin disabled without resubmitting its connection. Creating, replacing or enabling a connection must satisfy the current restrictions. Assigning a URL plugin to an agent does not activate it.
Tasks open their own client sessions to your existing MCP service. Completing or cancelling a task closes those clients and leaves the service and other tasks running. A task takes at most 200 plugin tools, counting plugins in the order they were assigned to the agent. The first plugin whose tools would pass that total is left out of the task, and so is every plugin assigned after it, even one small enough to fit; each is listed in the task’s history. A plugin with more than 200 tools assigned first therefore leaves the task no plugin tools, so assign large plugins last. The limit holds for the whole task: a plugin left out is not connected later, a plugin assigned while the task runs is left out if its tools do not fit, and a plugin whose tool list grows past the limit is withdrawn. An unavailable endpoint does not prevent other plugins from providing tools. Already admitted calls may finish before cleanup. Before each model turn after the first, the task reads the agent’s assignments and resolves each plugin once, secret included; the first turn uses what the task resolved when it started. A plugin that was removed, disabled, lost its endpoint or can no longer be authorized is withdrawn at that turn, and its client closes after its already admitted calls finish. A changed plugin or account is replaced by a client on the current assignment, and a renewed OAuth token or a rotated secret value reaches the open HTTP client without replacing it. If the assignments cannot be read at that moment, the plugins keep their clients for that turn. If the database or Vault is out of service while a plugin is resolved, the plugin keeps its client for at most three turns in a row or five minutes, and only while its assignment, connection and OAuth account are unchanged. A credential that is gone, a Vault path that Vault denies or does not hold, or a configuration the engine refuses withdraws the plugin at once. Between turns, each call rechecks the assignment, the plugin’s connection and the OAuth account, without reading any secret: a removed or reassigned plugin, a changed account, a disabled plugin or a reconnected or renewed OAuth account stops the call at once, while a rotated secret or a withdrawn credential grant takes effect at the next turn. There is no fallback to anonymous access or another account. A resumed task opens fresh sessions with the current selection. A failed connection is tried again at the next turn, once per turn, and the turn waits up to three seconds for a reconnection in progress: a server back in time gives its tools to that turn, and one still down holds a turn no longer than that. A failed call does not close its client. It may have reached the server, so its outcome is unknown and the task never replays it automatically. A Streamable HTTP server that answers 404 on a session, as the MCP specification has a server do for a session it ended, gets a new session at once: a listing goes to the new session, and a call is sent again on it, once, since the server processed nothing on the old one. The call is answered as not called instead when the new session lists that tool differently or cannot be opened. A call the engine had started when it stopped may have run too: when the task resumes, it answers that call as outcome unknown instead of calling it again, so the agent checks the call’s effect first, and the task details show the call in the MCP connection history. After checking, the agent may still call it again. A tool the agent selected after its plugin went away is answered with an error, and the task continues with the tools it still has; five such turns in a row with nothing else done fail the task. Tool lists are discovered on connection and reconnection, and again before each model turn. Each discovery reads the complete paginated list. A server that announces tool changes is listed again only after it announces one, or when its last listing is five minutes old; a server that does not announce changes is listed before every turn. A tool-list change notification immediately withdraws the affected tools until rediscovery completes, and a call refreshes the list first when the server announced a change. A tool keeps the same name for the whole task, so the tools offered to the model stay stable from turn to turn; a call planned against a replaced session or a changed input schema is refused, and the agent chooses the tool again from the current list. A failed discovery blocks only that MCP, while already admitted calls may finish and other clients remain usable. Tool schemas and the existing relevance filter are preserved, within limits that keep what every turn sends to the model bounded: a tool description longer than 2,000 characters is cut and ends with [truncated], a tool whose input schema is larger than 16 KiB is left out, and so is, in listing order, the first tool that takes the plugin’s tool definitions past 256 KiB, with every tool after it. Engine warnings name each tool cut or left out. An assigned plugin without an endpoint is recorded as migration-required and does not prevent other task work.
Task details show MCP connection observations from initialization and each resume, and only when there is something to report. After a successful credential change, the history also shows the previous and current credentials, by name when it can be read and by ID otherwise. Rotating the value behind an unchanged credential ID does not add an account-change entry. An OAuth replacement after a new consent records the previous and current authorization IDs and appears as a change of the organization’s connected account. An unavailable connection includes its plugin name, ID and failure category. A plugin that becomes unavailable while the task runs, or that is assigned during the task and cannot connect, is recorded once with its reason, including when it was disabled or unassigned. Earlier failures remain visible after a resume succeeds. Anyone allowed to read the task can read this history; it does not enter the agent’s dialogue. The agent is told which plugins it cannot use and why, including disabled ones and ones unassigned during the task, so it can report that instead of searching the host for them. That list is updated before every turn. Older tasks without observations have no history panel. These observations are not live connection checks. Engine warnings also include the task, agent, organization and plugin IDs, the failure category and the error behind it.
Choose the credential a plugin sends
One plugin has one connection method and, for Bearer or API key, one credential. Choose it in the plugin’s form, when adding the plugin or editing it, or setcredential_id in its connection through the API. It must be a Secret or Vault path in the plugin’s organization or shared across the tenant; a tenant-wide plugin takes only a tenant-wide credential. No token value appears in the picker or in any response.
Every agent assigned the plugin, directly or through its specialty, sends that credential. To reach one service two ways, or with two accounts, create two plugins and assign each to the agents that need it. Changing the credential or the URL of an enabled plugin needs no switch off: a running task’s next call is refused as changed, and its next turn connects with the new settings. A credential a plugin sends cannot be deleted, even while the plugin is disabled.
Use POST /api/v1/agents/{agentId}/plugins with { "plugin_id": "<plugin ID>" } to add one assignment, and POST /api/v1/agents/{agentId}/replace-plugins with { "plugin_ids": [...] } to save the agent’s whole set in one write. GET /api/v1/agents/{agentId}/plugins/{pluginId} returns the plugin and assignment_source, which says whether the agent was assigned it directly (agent) or inherits it from its specialty (specialty). It includes disabled assignments. Both read routes return only the agent’s own assignments by default; add include_inherited=true to include the plugins it inherits from its specialty. You need read access to both the agent and plugin. Auditors may read the assignment but cannot change it. Sessions and scoped API keys use the same routes. Agent creation takes plugin_ids.
A plugin the agent’s organization cannot use - unknown, not visible to you, or scoped to another organization - is refused with 400 BAD_REQUEST before anything is saved. field names the request field (plugin_id or plugin_ids) and details.plugin_ids lists the refused plugins. Detaching goes the other way: DELETE /api/v1/agents/{agentId}/plugins/{pluginId} answers 200 with the agent for a plugin the agent does not hold, unknown or not visible to you included, and changes nothing; only an inherited plugin is refused, with 409. A plugin whose stored connection can no longer be read is returned with connection: null by every read, the agent’s plugin list included, rather than failing it; it cannot be enabled until you save its connection again.
Give each agent its own token
For an MCP server where each agent signs in with its own token, configure the plugin as anonymous, give the agent the token as a credential in its accessible credentials, and the agent passes it in the tool arguments as{{secret:<name>}}. 2501 puts in the value just before the call leaves, under the same rules as a placeholder in a command, so the MCP server must accept the token as a tool argument. A hosted MCP server takes placeholders the same way. A placeholder the agent has no access to fails the call and nothing is sent, and a Bearer, API-key or OAuth plugin refuses placeholders, since it already authenticates with its own credential. The value is hidden in the tool result the agent reads, the task history and the ticket, and the recorded call keeps the placeholder. The MCP server writes the tool descriptions the agent reads, so it can ask for any credential the agent has: give that agent only the credentials the server may see.
Check an assigned connection
Open an agent’s detail page and select the plugin, including a disabled plugin. Save the agent, then select the flash icon beside the saved assignment; its tooltip reads Check connection. The result appears below the assigned plugins. To remove the assignment, select its X and save the agent. The form blocks saving if it cannot load assignments; use Retry before continuing. The result displays the check time, tool count, negotiated transport and protocol version, or a failure reason. It is local to the open form, clears when assignments change, and is not continuous monitoring. To use the connection in a task, separately enable the plugin. Bearer and API-key connections need the plugin’s credential, described above. A successful check is optional and does not grant runtime access. The equivalent API call isPOST /api/v1/agents/{agentId}/plugins/{pluginId}/check with no body. The caller needs permission to write the agent and read the plugin.
The engine initializes the saved endpoint, lists tools and closes its test client. For Bearer and API-key authentication, it resolves only the plugin’s Secret or Vault path after checking that it is still in the agent’s organization or tenant-wide; a draft without one fails the check. Invalid secrets and unresolved Vault values block the check without falling back to anonymous access or another credential. It calls no tool, deploys nothing and leaves the shared service running. You can check a disabled assignment without enabling it, except a hosted MCP server: a check starts the hosted MCP server, so only a tenant-level administrator can check one while it is turned off, and anyone else gets 403. The public agent plugin list contains enabled plugins by default; GET /api/v1/agents/{agentId}/plugins?include_disabled=true also returns disabled assignments.
A successful response contains success: true, agent_id, plugin_id, checked_at and tool_count, with the negotiated transport (streamable-http or http-sse) and protocol_version when the engine reports them. A failed check returns HTTP 200 with success: false and a failures list instead of tool_count, and, when the engine can say more than the category, a detail of at most 500 characters. For a remote MCP server it names the HTTP status, a URL that does not look like an MCP endpoint (it answered HTTP 404 or 405, with a web page or other content that is not MCP messages, named by its media type, or with JSON that is not an MCP message, such as a REST API’s root), a host name that does not resolve, a TLS certificate the engine does not trust, a failed TLS handshake, a refused connection (with the address and port tried) or a closed one, or a timeout, and never repeats what the server answered. For a hosted MCP server it is the start error, its credential values redacted, such as an image that cannot be pulled, a namespace quota used up, a role that cannot create pods or secrets, a handshake that timed out or all 32 hosted MCP servers in use. The agent’s plugin list shows it after the failure. Categories are invalid_configuration, connection_or_discovery_failed, cleartext_not_allowed, destination_not_allowed, redirect_not_followed, access_check_failed, credential_unavailable (the bound Secret or Vault path could not be read), authorization_required (the organization has no connected OAuth account), authentication_failed (the MCP server answered HTTP 401 or 403 to the plugin’s credentials), image_unavailable (the engine could not get a hosted MCP server’s image), migration_required, assignment_changed, cleanup_failed, hosted_server_starting (the check waited 45 seconds for a hosted MCP server that is still starting; the start goes on, so check again in a minute) and hosted_server_start_failed (the same, for a hosted MCP server whose last start failed; detail gives that failure, such as a handshake that timed out, so fix the image, arguments or variables before checking again). An unknown or inaccessible assignment returns 404; a failure to reach the engine returns an API error.
The result is an instant snapshot. Success requires that the assignment, enablement, connection and credential access still match when the client closes. Bearer and API-key checks resolve the plugin’s credential again before returning success, so a changed Vault value also invalidates the result. The check does not prove access to individual operations or guarantee that a later task can connect. Checking is optional and does not change activation or runtime authorization.
Authorize an OAuth account
On the Plugins page, select OAuth and choose a pre-registered client, automatic registration or dynamic registration. For a pre-registered client, enter its client ID. Leave Use the provider’s requested scopes selected, or enter explicit scopes. A confidential client names an existing Secret or Vault path credential as its client secret, chosen in the plugin form; agents need no grant for it, and its value is never pasted into the plugin settings. The saved plugin page keeps these OAuth settings visible beside the connected account. Save the plugin, then select Connect account in the plugin editor. Only someone who can edit the plugin connects it: an organization administrator for the organization’s plugin, and a tenant-level administrator for a plugin shared with every organization, from each organization that uses it. Save any other edit first: connecting waits for the plugin to be saved. Agents assigned in the organization share the account. Depending on the provider, you connect in one of three ways, and Command Center picks the one that applies:- Automatic return. Select Open the sign-in page, which opens the provider in a new tab, or Copy the link to open it elsewhere. After you consent, the provider sends your browser back to Command Center, which confirms the connection; Open Plugins returns you to the plugin list. When it cannot, the page says why. If your Command Center session ended before you came back, you are asked to sign in again, and the page then tells you to start the connection again.
- A code shown in Command Center. When the provider supports device sign-in, the editor shows a code and the provider’s address instead. Open that address on any device that can reach the provider, enter the code and sign in; the editor notices on its own and says the account is connected. A refused or expired code offers Start again. A provider that does not let the client use device sign-in gets the sign-in page instead, and a check the provider fails to answer, or answers with an outage, is asked again later; anything else ends the code and says why.
- Paste the return address. If the page the provider sends your browser back to does not load, copy the whole address from the address bar, paste it into Paste the address your browser ended on, and select Complete. Command Center checks it exactly as it checks an automatic return. Only you can complete a sign-in you started, and only once.
"grant": "client_credentials" on an OAuth authentication whose client is pre-registered with a client_secret_credential_id. The editor shows whether the account is connected or its last renewal failed. A service account is asked a new token for again a minute after a failed renewal, so once its client secret or private key, or the provider, is fixed it recovers by itself, at the next check or task; Check connection establishes whether the provider accepts it.
The equivalent API configuration for a pre-registered public client is:
POST /api/internal/plugins/{pluginId}/oauth/start; other sessions get 403, and the same holds for the device poll, the service account and completing a sign-in; an org-scoped plugin needs no body, a tenant-wide plugin takes { "org_id": "org_…" }. The answer has flow: "redirect" with an authorization_url and the client_registration it signs in with, or flow: "device" with a user_code, a verification_uri, sometimes a verification_uri_complete, and an interval in seconds. For a redirect, open authorization_url in the same browser session and complete the provider consent screen. The address asks the provider to let you choose the account (prompt=select_account, or prompt=login when the provider lists the prompts it supports without that one), so a session already open at the provider does not connect its account without you choosing it; the provider returns to the deployment’s fixed callback URL, which sends you to a Command Center page confirming the connection. If that page does not load, send the address the browser ended on to POST /api/internal/mcp-oauth/complete as { "url": "…" }. For a device sign-in, call POST /api/internal/plugins/{pluginId}/oauth/device/poll with the same body every interval seconds: it answers pending with the interval to wait, connected, denied or expired. No token is returned. API keys cannot start or complete this user consent flow. GET /api/v1/plugins/{pluginId}/oauth answers with the organization’s account, and its read-only account_name says who it signs in as: the preferred_username, email or subject from the ID token the provider returns when the plugin’s scopes include openid, otherwise from the provider’s OpenID Connect user info endpoint, and the client ID for a service account; null when the provider says none of these. The plugin’s account panel shows it (“Connected as …”), and so does a successful connection check (account_name).
Register the redirect URL the plugin form shows under Redirect URL to register with your provider, with its copy button. Each authorization server has its own, https://your-command-center-host/api/internal/mcp-oauth/callback/<key>, where the key comes from the server’s issuer, so the form shows it once detection has found the provider, or after a first connect. Plugins on the same provider share it. A pre-registered client saved before this address existed, or created through the API without "callback": "per_provider" on its client, keeps the address every provider shares, https://your-command-center-host/api/internal/mcp-oauth/callback, and the form shows that one for it. The deployment’s BASE_URL determines the host of both: the engine builds the address from its own BASE_URL, so the engine and Command Center must have the same one. The provider sends the browser back to that address after consent, so BASE_URL is the address users open Command Center with, HTTPS where you can. A plain-HTTP address, as on an installation reachable only on its own network, is accepted: some providers refuse to send the browser back to one, and the code sign-in or pasting the return address then still complete the sign-in. The plugin editor says when Command Center is served over plain HTTP, and the engine logs a warning once. Only a missing BASE_URL stops the sign-in. The Client ID Metadata Document still needs an HTTPS address that resolves publicly. A sign-in expires after ten minutes, or when the provider’s device code does, and can be completed only once by the user who started it. Starting again replaces your previous attempt, not a colleague’s. The account already connected keeps working throughout, and is replaced only once a new one is authorized, so closing or refusing the consent screen disconnects nothing. Check an assignment, then enable the plugin once every organization with a live assignment has connected its account; an enabled plugin only accepts new assignments in organizations that did.
A pre-registered client becomes confidential when it names an existing Secret or Vault path credential in client.client_secret_credential_id, and client_secret_basic or client_secret_post is then chosen from what the provider advertises. The credential must be readable by every organization the plugin serves, so a tenant-wide plugin needs a tenant-wide credential, and agents need no grant for it. This credential holds the client secret, not an access token. Giving it a new value keeps the connected accounts: the provider still honours what it granted, and the new value is used from the next token request on, so it has to match what the provider expects. Making the credential tenant-wide keeps them too. Moving it into one organization is refused while a plugin outside that organization uses it: choose another credential in that plugin first. Naming a different credential does require connecting the account again. With client: { "registration": "automatic" }, 2501 needs no client created on the provider. When the provider advertises Client ID Metadata Documents and BASE_URL is an HTTPS address that resolves to public addresses, the client ID is the address of the document Command Center publishes at /api/mcp-oauth/client, which the provider fetches. Otherwise, including when the provider cannot reach this installation, 2501 registers a client dynamically with the exact callback, once per provider: a public client, or a confidential one when the provider takes no public client, whose secret 2501 keeps. The registration names no scopes, so the provider gives the client its default ones; the plugin’s scopes are asked for at sign-in. On Keycloak, a scope asked for must be one of the realm’s optional client scopes (a client scope of type Optional), and a provider that limits a client to the scopes it registered needs a pre-registered client. A registered client is reused until the provider’s expiry for it, a sign-in still in flight reuses the client registered for it, and a client the provider no longer accepts is registered again: at once for a device code, at the next connect otherwise. With client: { "registration": "dynamic" }, 2501 always registers dynamically, even with a provider that reads metadata documents. Choose it when the provider cannot fetch the document from Command Center, for example behind an inbound firewall: a sign-in with the metadata document that expires without returning suggests it. A provider that offers neither, or refuses the registration, needs a pre-registered client, and connecting says so. For Microsoft Entra ID, which refuses the resource parameter, 2501 leaves it out.
A plugin trusts one authorization server, and sends client secrets, codes and tokens to that server only. When the plugin form detected it, saving the plugin trusts it (trusted_issuer on create or update, ignored when the plugin already trusts one). Otherwise the first connect trusts the server the MCP server names. The editor shows the trusted server, for example “Trusts login.example.com”. If the MCP server later names another one, connecting is refused with a message saying so. When the move is expected, someone who can edit the plugin chooses Forget trusted provider (DELETE /api/v1/plugins/{id}/oauth/trusted-issuer): the connected accounts stay, their renewals fail until a server is trusted again, and that person’s next connect trusts the server named then. Changing the plugin’s connection or scope forgets the trusted server along with the connected accounts, and ends any sign-in in progress: a device sign-in approved at the provider afterwards polls expired, and a browser return cannot finish. Start the sign-in again. The provider’s addresses must be public HTTPS addresses. An installation whose identity provider sits on its own private network sets MCP_OAUTH_ALLOW_PRIVATE_PROVIDERS=true on the engine; without it, detection and connecting both say the provider is on a private address and name that setting.
The provider must advertise PKCE S256 for the sign-in page (Microsoft Entra ID, which takes it without saying so, excepted), a client authentication method 2501 supports, HTTPS endpoints and resource metadata identifying its issuer and resource. An MCP server that publishes no resource metadata signs in at its own origin: detection offers OAuth there, and saving the plugin trusts that origin like any other provider. Unless the installation allows private providers, such a server on a private address is offered a token instead. The return to Command Center must name the provider (the iss parameter) when the provider says it does. A return arriving at a provider’s own redirect URL was sent to that server’s address, so that is all; 2501 registers its clients there, and so do pre-registered clients set up in the plugin form. The address does not prove which client the return is for: at a provider with open client registration, anyone can register a client with that address and send a code there, and only the sign-in’s state and PKCE verifier keep that code from completing it. At the shared address the return must also name the provider for a metadata document and for a pre-registered client without a client secret, so such a client needs a provider that returns iss (RFC 9207), as Keycloak does, a client secret, or device sign-in. Tokens must use Bearer authentication and cover the requested scopes. An access token may use any visible ASCII character except quotes, angle brackets and backslash, and holds at most 8192 characters; a longer one fails sign-in or renewal. A provider that omits expires_in is accepted; the token then has no expiry (expires_at is null) and serves until an MCP server refuses it. Access and refresh tokens remain private to the plugin authorization and are excluded from API serialization and agent CSV/JSON or CLI imports.
When less than a minute or a tenth of its lifetime is left, whichever is longer, the next connection check renews an access token if the provider supplied a refresh token. A running task checks its connections before each turn, when the token must also outlast the turn, seven minutes at most, and again before each call. An MCP server that answers 401 processed nothing: the token is renewed however long it has left, and a call is sent again once with it; a connection it refused reconnects with the new token at the next turn, and a connection check tries again at once. Concurrent checks share one exchange and keep the same authorized account, across engine processes too, so a refresh token the provider rotates is used once. An open task session takes the new token without reconnecting, and the renewal does not appear as an account change. Automatic registration asks for refresh support: the metadata document declares it, and dynamic registration requests it when the provider advertises it. For a pre-registered client, enable the provider’s refresh-token grant if you want automatic renewal. Nothing renews an account in the background: an access token that expired while nobody used the plugin reads Connected, renews on next use when the account holds a refresh token or is a service account, and Reconnect needed when it holds neither. GET /api/v1/plugins/{id}/oauth says which in authorization.renewable.
If no refresh token is available, renewal fails or its outcome is uncertain, the check returns authorization_required once the current token has expired. An account without a refresh token, or whose refresh token the provider refuses, needs consent again to restore access. A service account whose client secret was fixed, and an account whose refresh token the provider still accepts, after an outage for example, recover by themselves at the next renewal. The connected account is kept meanwhile: a provider that refuses and a provider that cannot be reached are indistinguishable here, so nothing is discarded on a guess. A failed renewal is left alone for a minute before the next check or task tries again, and the account panels say the last renewal failed and why: refused by the provider, a provider that could not be reached, a token 2501 does not accept, another authorization server named, no authorization server trusted yet, no refresh token, a client secret credential that could not be read, a client the provider no longer accepts, a provider on a private address, a saved connection that is not usable, or another error the engine logged. A missing scope the plugin’s own scope list leaves out is shown with a prompt to add it there, since reconnecting never asks beyond that list. The platform never replays an MCP call, except once after a 401, which the server answers before processing anything. A token without an expiry, or one the provider revoked early, stops only when the MCP server refuses it.
If a server refuses a call because the connected account lacks a scope, the call fails with a message naming it and the account shows Reconnect needed with scopes followed by the scopes the server asks for. Reconnecting the account, a service account included, asks the provider for them as well; nothing asks for them on the server’s word alone. A plugin that lists its own scopes never asks beyond them, and when the provider or the server lists the scopes it knows, only those are asked for. A scope the provider refuses is no longer asked for: connect again.
Two optional fields of the OAuth connection, under Advanced in the plugin form, cover providers that need more:
audience: the API the tokens are for, sent as theaudienceparameter on the sign-in page and on every token and device request, service accounts included. Auth0 needs it to issue a token for an API rather than refuse the service account (“No audience parameter was provided”).metadata_url: the HTTPS address of the authorization server’s metadata document, for a provider whose metadata does not live at its issuer. Azure AD B2C publishes it under a user flow, for examplehttps://<tenant>.b2clogin.com/<tenant>.onmicrosoft.com/<user flow>/v2.0/.well-known/openid-configuration, and names a tenant ID issuer. Sign-in and renewal then read that document instead of the server the MCP server names, and the plugin trusts the issuer the document names. The issuer and the endpoints the document lists must be on the document’s own origin.
private_key_jwt): in the plugin form choose Client authentication: Private key (JWT), pick the Secret or Vault path credential that holds the PKCS #8 PEM private key, and enter the key ID (kid) the provider knows the public key by, if it asks for one, and the algorithm (RS256 by default, or ES256). Through the API, set client.private_key: { "credential_id": "cred_…", "key_id": "…", "algorithm": "RS256" } in place of client_secret_credential_id. Okta’s org authorization server requires it for a service account. Every token request then carries a JWT signed with the key for the provider’s token endpoint, valid for a minute. A provider that lists its client authentication methods without private_key_jwt is refused as unsupported. A key that cannot sign, because it is not PEM or not for the algorithm, is refused before the provider is opened.
Connect Microsoft Entra ID
Entra ID does not register clients on its own, so create the client there first:- In the Entra admin center, open App registrations and select New registration. Under Redirect URI, choose Web and enter the redirect URL the plugin form shows for your tenant’s issuer,
https://your-command-center-host/api/internal/mcp-oauth/callback/<key>: start the plugin with the MCP server’s URL and copy it from there. - Under Certificates & secrets, create a client secret. Store its value in 2501 as a Secret credential and name it as the plugin’s client secret.
- Under API permissions, add the delegated permissions the MCP server expects, usually a scope of its own app registration such as
api://<application ID>/mcp.read, and grant admin consent if your tenant requires it. - To let people sign in with a code instead, under Authentication, turn on Allow public client flows. Connecting then shows a code first.
offline_access so 2501 receives a refresh token, and openid profile to show who signed in. Entra issues a token for one API at a time, so do not mix the MCP server’s scope with Microsoft Graph scopes. The trusted provider is your tenant’s issuer, https://login.microsoftonline.com/<tenant ID>/v2.0. An MCP server that names a multi-tenant endpoint (common or organizations) is trusted as that endpoint, for example https://login.microsoftonline.com/common/v2.0: people from any tenant can then sign in, and a service account is not available, since it needs a tenant.
For a service account, add an Application permission of the MCP server’s app registration instead, grant admin consent, select Sign in as a service account, and enter the single scope api://<application ID>/.default.
Give a legacy plugin an endpoint
Before upgrading, have the deployment owner stop the old engine and verify that its owned plugin containers or pods are gone. Remove any remaining owned resources explicitly, preserving independent executors. This step performs no cleanup of its own. Deploy the replacement MCP service, or run the server as a hosted MCP server: a server shipped as a container image can be a hosted MCP server, which 2501 runs itself. Create it as a new plugin with that image, under a new name: the legacy plugin, such asvmware, keeps its name. Assign the new plugin to the agents that used the legacy one, then turn the legacy plugin off or delete it. The VMware MCP that the removed runtime started, ghcr.io/2501-ai/vmware-mcp, is such a server: its variables are GOVC_URL, GOVC_USERNAME, GOVC_PASSWORD (a credential) and GOVC_INSECURE=1 for a self-signed certificate. On Kubernetes, allow the vCenter’s address range for hosted MCP server pods with a policy of your own, since they reach only public addresses by default.
To serve it as one shared HTTP endpoint instead, run it behind an HTTP wrapper such as mcp-proxy, for example mcp-proxy --port 8080 -- vmware-mcp in a container built from that image. Give that service the vCenter settings in its own environment: GOVC_URL, GOVC_USERNAME, GOVC_PASSWORD, and GOVC_INSECURE=1 for a self-signed certificate. Set MCP_PROXY_API_KEY so the wrapper refuses a request without that key in its X-API-Key header. The endpoint is then http://<service>:8080/mcp, with the API-key method on the X-API-Key header and the key as the plugin’s credential.
Install mcp-proxy when the image is built, so that the container needs no internet access when it starts. The VMware image ships Bun; in an image with Node.js, run npm install mcp-proxy@6.7.19 instead:
- Open the unconfigured plugin’s editor on the Plugins page and choose Add an endpoint.
- Enter the deployed MCP URL and authentication method, turn the plugin off, then save.
- Reload and verify the saved connection. The plugin keeps its ID and agent assignments and remains disabled.
- For a Bearer or API-key endpoint, choose the credential the plugin sends; for OAuth, connect the account of each organization. Check each saved connection, then enable the plugin.
VMware settings
The removed runtime ran the image the engine settingVMWARE_MCP_IMAGE named (vmware-mcp:latest by default) and passed it every engine variable whose name starts with VMWARE_, unchanged. ghcr.io/2501-ai/vmware-mcp reads the variables of govc, the vSphere command line it runs, instead. Each setting therefore moves to a variable of the new plugin when it is a hosted MCP server, or of the wrapper’s container:
Other govc variables, such as
GOVC_DATACENTER, set its defaults as usual.

