> For the complete documentation index, see llms.txt.
Skip to main content

Check out Port for yourself ➜ 

Virtual MCP servers

Closed Beta Feature

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.

MCPs page showing the External MCPs, Virtual MCPs, and Virtual Tools tabs

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").

Step-by-step example

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:

PropertyDescription
IdentifierA 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 -.
TitleThe server's display name in Port.
DescriptionWhat the server is for.
InstructionsInstructions sent to the agent when it connects. When empty, the default Port MCP server instructions are sent.
Passthrough ToolsRegex patterns of tools to expose unchanged. See passthrough patterns.
Virtual ToolsA relation to the virtual tools this server exposes.
URLThe 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. Use list_.* to match every tool that starts with list_. 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.
PatternExposes
list_entitiesThe 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.

Use the connector identifier, not its title

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.

PropertyDescription
Exposed nameThe tool name the agent sees and calls. Must be unique within each server that uses the tool.
Underlying toolThe tool to customize: a Port tool such as list_entities, or an MCP connector tool as <connector_identifier>_<tool>.
DescriptionThe description the agent sees. When empty, the underlying tool's description is used.
Expose argumentsWhich arguments the agent sees: all or listed. See expose arguments.
Argument overridesPer-argument rules, keyed by the argument's path in the underlying tool's input. See argument overrides.
Virtual tools and passthrough patterns

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:

FieldWhat it does
valueHides the argument and always sends this value.
mergeWith value on an array argument: keeps the argument visible and adds your items to the agent's.
enumAllows only the listed values.
asShows the argument to the agent under a different name.
descriptionShows your description instead of the original one.
exposeOn 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" }
Fixed blueprint

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"
}
Always-on filter

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: enum only 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"] }
Allowed values

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" }
Renamed argument

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." }
Custom description

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." }
Listed nested arguments

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:

  • value and merge: on arrays, lock some items and let the agent add its own.
  • as, description, and enum: 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, an enum on entity.identifier does not stop the agent from leaving out the identifier or writing to another blueprint. To limit the tool to one record, fix both blueprintIdentifier and entity.identifier with value, 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:

https://mcp.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​

  1. Add the virtual MCP server URL as a Custom Server MCP connector.
  2. 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 inputs on trigger_run or entity.properties on upsert_entity. Set the whole object with value instead, 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​

Where to go next​