Virtual MCP servers
This feature is currently in closed beta with limited availability.
To request access, please contact Port's support team.
When you connect an agent to an MCP server, it gets every tool that server holds. Connect a few servers and the agent carries dozens of tools it does not need for its job:
- Token cost: every tool definition lands in the agent's context, so each server you add raises token use on every request.
- Wrong tool picks: the longer the tool list, and the more look-alike tools it holds (such as "create issue" in both a Jira and a Linear MCP), the more often the agent picks the wrong one.
- All-or-nothing access: an agent that only needs to read pull requests also gets the tool that merges them.
- No control over behavior: once connected, the agent decides what to send to each tool. A system prompt can ask it to behave, but nothing enforces it.
A virtual MCP server lets you govern an agent's tools: what it can see, what it can call, and how it behaves when it does. Each agent loads only the tools its job needs, under names it can tell apart, and the values you fix hold on every call, however the agent is prompted. A platform engineer builds the server once in Port, with no code to write or host, and each team connects to it through its URL.
How it works
A virtual MCP server is an MCP server with its own URL. It connects to the Port MCP server and to your MCP connectors, and exposes a selection of their tools in two ways:
- Passthrough tools: regex patterns that expose existing Port or MCP connector tools unchanged. See passthrough patterns.
- Virtual tools: customized versions of a single Port or MCP connector tool, with their own name, description, and argument rules. See customize a tool.
Agents connect to the server from any MCP client, such as an IDE, Port AI, or a custom agent. See connect a virtual MCP server.
Every tool runs with the permissions of the user who calls it. A virtual MCP server can only narrow what an agent sees. It never grants access the user does not already have.
Create a virtual MCP server
You can create and edit virtual MCP servers from the Virtual MCPs tab of the MCPs page, from the catalog like any other entity, or by asking Port AI (for example, "Create a virtual MCP server for incident response that only exposes read tools").
To build a scoped server end to end, from passthrough tools to locked write tools, follow the build an MCP server scoped to one agent task guide.
Each virtual MCP server has the following properties:
| Property | Description |
|---|---|
| Identifier | A unique ID for the server. Port uses it as the last part of the server's URL, so it can only contain letters, numbers, _, and -. |
| Title | The server's display name in Port. |
| Description | What the server is for. |
| Instructions | Instructions sent to the agent when it connects. When empty, the default Port MCP server instructions are sent. |
| Passthrough Tools | Regex patterns of tools to expose unchanged. See passthrough patterns. |
| Virtual Tools | A relation to the virtual tools this server exposes. |
| URL | The server's URL, built from its identifier: https://mcp.port.io/v1/virtual-server/<identifier>. See connect a virtual MCP server. |
By default, every user in your organization can view virtual MCP servers and virtual tools, and only admins can create or edit them. To change this, set catalog permissions on the virtual MCP server and virtual tool blueprints.
Passthrough patterns
Each entry in Passthrough Tools is a regex pattern, and Port applies two rules:
- The pattern must match the whole tool name. Port anchors every pattern for you, so
list_matches nothing. Uselist_.*to match every tool that starts withlist_. Do not add^or$. - Only patterns that start with
<connector_identifier>_match connector tools. Every other pattern matches Port tools only. This includes.*, and it includes^github_.*: because of the leading^, the pattern no longer starts with the connector identifier, so it does not expose any GitHub connector tool.
| Pattern | Exposes |
|---|---|
list_entities | The Port list_entities tool. |
list_.* | Every Port tool whose name starts with list_. |
.* | All Port tools. |
<connector_identifier>_.* | All tools of one MCP connector, for example github_.*. |
<connector_identifier>_<tool> | One tool of an MCP connector, for example github_list_commits. |
For more patterns, such as read-only or entity-only tool sets, see common tool selection patterns. Port AI uses the same pattern shapes. In a virtual MCP server, check that each one matches whole tool names.
MCP connector tools are named <connector_identifier>_<tool_name>, where <connector_identifier> is the connector's identifier in Port. A connector titled "GitHub Enterprise" with the identifier github_ent needs the pattern github_ent_.*, not GitHub Enterprise_.*. You can see the full name of each connector tool in the External MCPs tab of the MCPs page.
Start the pattern with the identifier itself, with no leading ^. Port AI agents route tool patterns the same way.
Example: an incident responder server (click to expand)
This server exposes two Port tools and every tool of a connector with the identifier pagerduty:
{
"identifier": "incident-responder",
"title": "Incident responder",
"properties": {
"description": "Read-only tools for investigating incidents.",
"instructions": "You help on-call engineers investigate incidents. Start by listing open incidents.",
"passthrough_tools": ["list_blueprints", "list_entities", "pagerduty_.*"]
}
}
Agents connect to it at https://mcp.port.io/v1/virtual-server/<identifier>, where <identifier> is incident-responder.
Customize a tool
A virtual tool is a customized version of one underlying tool. Use it to rename a tool, rewrite its description, or fix and limit its arguments. Create virtual tools from the Virtual Tools tab of the MCPs page, then add them to a server through its Virtual Tools relation. You can add the same virtual tool to several servers.
| Property | Description |
|---|---|
| Exposed name | The tool name the agent sees and calls. Must be unique within each server that uses the tool. |
| Underlying tool | The tool to customize: a Port tool such as list_entities, or an MCP connector tool as <connector_identifier>_<tool>. |
| Description | The description the agent sees. When empty, the underlying tool's description is used. |
| Expose arguments | Which arguments the agent sees: all or listed. See expose arguments. |
| Argument overrides | Per-argument rules, keyed by the argument's path in the underlying tool's input. See argument overrides. |
When a virtual tool and a passthrough pattern produce the same tool name, the virtual tool takes precedence. If your virtual tool has a new name, the original tool stays available through any passthrough pattern that matches it. To make the agent use only your version, remove or narrow that pattern.
Expose arguments
Expose arguments decides which of the underlying tool's arguments the agent sees:
all(default): the agent sees every argument of the underlying tool. Use it when you only need to fix or adjust a few arguments and leave the rest as they are.listed: the agent sees only the arguments that have an entry in Argument overrides. If the agent sends any other argument, Port drops it before it calls the underlying tool. Use it for write tools, where you want a short contract you fully control.
In both modes, an argument fixed with value is always hidden from the agent and always sent with your value.
With listed, every required argument of the underlying tool needs an entry. Port checks this when you save the virtual tool. For each required argument, either add an empty entry, such as "blueprintIdentifier": {}, so the agent fills it, or fix it with value.
Argument overrides
Argument overrides is a set of rules, one per argument. Each rule is keyed by the argument's path in the underlying tool's input, with dots for nested fields, such as blueprintIdentifier, query.rules, or entity.identifier. Overrides change the tool schema the agent sees, and Port applies them again on every call, whatever the agent sends.
Each rule can use the following fields:
| Field | What it does |
|---|---|
value | Hides the argument and always sends this value. |
merge | With value on an array argument: keeps the argument visible and adds your items to the agent's. |
enum | Allows only the listed values. |
as | Shows the argument to the agent under a different name. |
description | Shows your description instead of the original one. |
expose | On an object argument: listed shows only the nested arguments that have an entry. |
Port validates the virtual tool against the underlying tool's schema when you save it, and rejects rules that would not work at runtime.
Fix a value
Use the value field when the agent must not choose the argument. The agent does not see it, and Port always sends your value. Typical cases are the blueprint to query or the channel to post to.
"blueprintIdentifier": { "value": "incident" }
The agent can only query incidents, and never sees a blueprint argument.
Add to an array
Use the merge field with value on an array argument when the agent should keep the argument but must not drop your items. append adds your items after the agent's, and prepend adds them before. On list_entities, this keeps a filter rule in every query:
"query.rules": {
"value": [{ "property": "status", "operator": "=", "value": "open" }],
"merge": "append"
}
The agent can add its own filter rules, and every query also includes status = open.
merge only works on arrays. You cannot add to a string argument, such as a free-text search query. Use description to tell the agent what the query must include, or fix the whole query with value.
Limit allowed values
Use the enum field as an allow-list. Port checks it in two places:
- When you save the virtual tool:
enumonly works on string, number, and boolean arguments, and cannot be empty. Port also checks each allowed value against the underlying argument's type, so"enum": ["3"]on a number argument is rejected. - On every call: the agent sees the allowed values in the tool's schema. If it still sends another value, Port does not call the underlying tool. It returns an error to the agent, such as
Invalid value for "cluster". Allowed values: "test", "staging"., and the agent can retry with an allowed value. Port never drops or replaces the value on its own.
For example, on a connector tool that redeploys a service:
"cluster": { "enum": ["test", "staging"] }
The agent can redeploy to test or staging. A call with production fails with an error, and nothing is redeployed.
enum does not make an optional argument required. If the argument is optional, the agent can leave it out, and the call goes through without it. When the argument must always be set, use value instead.
Rename an argument
Use the as field when the original name is vague or does not match your organization's terms. The agent sees and fills the argument under your name. On each call, Port maps it back to the original name before it calls the underlying tool, so the tool itself does not change. The original name is no longer available to the agent.
"blueprintIdentifier": { "as": "catalog_type" }
The agent fills catalog_type, and Port sends it to the tool as blueprintIdentifier.
Rewrite a description
Use the description field so the agent sees your description instead of the original one. Use it to say what to fill in and where to find it, especially for arguments you cannot lock, such as a free-text query or a time the agent must supply.
"query.rules": { "description": "Filter rules. Always include a rule on the service property." }
The agent reads your guidance when it fills the filter rules. The value itself stays up to the agent.
Limit nested arguments
Use the expose field on an object argument. expose: "listed" shows the agent only the nested arguments that have their own entry. The other nested arguments are hidden, the same way Expose arguments works on the top level. In this example, the agent sees query.rules and nothing else inside query:
"query": { "expose": "listed" },
"query.combinator": { "value": "and" },
"query.rules": { "description": "Filter rules for the incidents to list." }
Inside query, the agent only fills the filter rules. The combinator is always and.
A required nested argument hidden this way must have a value. Otherwise, Port rejects the virtual tool when you save it.
Combine overrides
You can use several fields on the same argument, and rules on different arguments together:
valueandmerge: on arrays, lock some items and let the agent add its own.as,description, andenum: rename an argument, explain it, and limit its values at once, for example{ "as": "catalog_type", "description": "The kind of record to list.", "enum": ["service", "incident"] }.- Constraints that must travel together: fix related arguments together. On
upsert_entity, anenumonentity.identifierdoes not stop the agent from leaving out the identifier or writing to another blueprint. To limit the tool to one record, fix bothblueprintIdentifierandentity.identifierwithvalue, and create one virtual tool per record.
Example: list open incidents
This virtual tool turns list_entities into a tool that can only list open incidents:
{
"identifier": "list_open_incidents",
"title": "List open incidents",
"properties": {
"name": "list_open_incidents",
"tool": "list_entities",
"description": "List open incidents. Read only. Use it to find incidents to investigate.",
"arguments": {
"blueprintIdentifier": { "value": "incident" },
"query.combinator": { "value": "and" },
"query.rules": {
"value": [{ "property": "status", "operator": "=", "value": "open" }],
"merge": "append"
}
}
}
}
The agent sees a list_open_incidents tool with no blueprint argument. It can add its own filter rules, but the status = open rule is always added, and the combinator is fixed to and, so its rules can only narrow the results.
Connect a virtual MCP server
Each virtual MCP server has its own URL, shown in its URL property. Use the URL for your Port region:
- EU region
- US region
https://mcp.port.io/v1/virtual-server/<identifier>
https://mcp.us.port.io/v1/virtual-server/<identifier>
Replace <identifier> with the virtual MCP server's identifier.
From your IDE
Connect your IDE or MCP client the same way as the Port MCP server, using the virtual MCP server URL instead. Your client authenticates with Port through OAuth, and each tool call runs with your permissions. See Port MCP server installation for client-specific steps.
From Port AI and custom agents
- Add the virtual MCP server URL as a Custom Server MCP connector.
- Select the connector in an agent's MCP servers field, as described in build an AI agent.
Limitations
- Virtual MCP servers expose tools only, not prompts.
- If an underlying tool's schema changes after you save a virtual tool, calls to that virtual tool fail until you update it.
- A virtual MCP server cannot expose another virtual MCP server's tools.
- You cannot override a single key inside a free-form object argument, such as
inputsontrigger_runorentity.propertiesonupsert_entity. Set the whole object withvalueinstead, or leave it unchanged. - Only a user who is connected to an MCP connector can save a virtual tool over that connector's tools. API and Terraform tokens cannot save these virtual tools.
FAQ
Not through that virtual MCP server. A virtual MCP server limits what an agent sees, not what the user is allowed to do. Users can still reach any tool they have access to through the Port MCP server or their MCP connectors.
Changes apply the next time the MCP client loads the server's tool list. Most clients load it when they connect, so ask the agent to reload the server's tools, refresh the server in your client's MCP settings, or reconnect the client.
Port builds the tool list separately for each user who connects. A virtual tool appears only when that user can use the tool it wraps. Port hides it when:
- The underlying tool is a Port tool the user does not have access to.
- The underlying tool comes from an MCP connector the user has not connected to. Connect to the connector from the External MCPs tab of the MCPs page or the Port AI chat, as described in authenticate to connectors.
After you fix access, reload the server's tools in your client.
Check the following, in order:
- URL and region: the URL ends with the server's identifier, not its title, and uses the host for your region (
mcp.port.iofor EU,mcp.us.port.iofor US). - Authentication: your client completed the OAuth flow with Port, in the organization that owns the server.
- Connector access: you are connected to every MCP connector whose tools the server exposes. Tools from connectors you have not connected to do not appear.
- Passthrough patterns: if the server shows no tools, check that each pattern matches whole tool names and starts with the connector's identifier, not its title. See passthrough patterns.
- Stale tool list: reload the server's tools or reconnect the client after any change.
Port checks every virtual tool against the underlying tool's schema when you save it, and blocks rules that would fail at runtime. The most common errors:
Required argument "<path>" is hidden by expose "listed" and has no value: you set Expose arguments tolistedand left out a required argument. Add an empty entry for it ("<path>": {}) so the agent can fill it, or fix it withvalue. See expose arguments.sets enum, which only applies to a string, number, or boolean argument:enumcannot limit an object or array argument. Fix the whole argument withvalueinstead.- An
enumvalue is invalid: an allowed value does not match the argument's type, such as a string on a number argument. - A path does not resolve: the argument path points inside a free-form object, such as
inputs.workloadontrigger_run. Set the whole object withvalue. See limitations. - Connector tool cannot be resolved: you are not connected to the MCP connector the tool comes from. Connect to it, then save again.
Where to go next
- Build an MCP server scoped to one agent task: build a scoped server step by step.
- MCP connectors: add the external MCP servers a virtual MCP server can draw tools from.
- Port MCP server available tools: see the Port tools you can pass through or customize.
- Tools and approvals: control which tools Port AI can use and when it asks for approval.