Machine authentication for the Port MCP server
When you connect the Port MCP server from CI/CD pipelines, GitHub Actions, or other automated environments, interactive OAuth (browser login) is not practical. Port supports machine-to-machine authentication using your existing Port client ID and client secret through the MCP OAuth token endpoint.
Authentication methods compared
| Method | Best for | Long-lived secret | User consent |
|---|---|---|---|
Interactive OAuth (authorization_code + refresh) | Cursor, VS Code, Claude Desktop | Refresh token (up to ~30 days) | Yes |
OAuth client_credentials (MCP token endpoint) | CI/CD, headless agents, mcp-remote | Port client ID + secret | No |
Your client ID and client secret are long-lived. The access token they produce is short-lived (typically about 3 hours). Re-exchange your credentials when the token expires. There is no refresh token for the client_credentials grant.
Prerequisites
- Create a Port client ID and client secret in your Port organization settings.
- Use a dedicated service account credential for automation, not a personal user's credentials.
- Store secrets in your CI secret manager. Never commit them to source control.
Request an access token
Port's MCP server exposes a standards-compliant OAuth 2.0 token endpoint that accepts the client_credentials grant. It returns a machine JWT (isMachine: true) that you pass as a Bearer token when connecting to MCP.
Discover supported grants
curl -s "https://mcp.port.io/.well-known/oauth-authorization-server" | jq .
The response includes:
token_endpoint:https://mcp.port.io/v1/token.grant_types_supported:authorization_code,refresh_token,client_credentials.token_endpoint_auth_methods_supported:none,client_secret_post,client_secret_basic.
For US-hosted Port, replace mcp.port.io with mcp.us.port.io.
Request a token (client_secret_post)
curl -s -X POST "https://mcp.port.io/v1/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
Example response:
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 10800
}
Request a token (client_secret_basic)
curl -s -X POST "https://mcp.port.io/v1/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \
-d "grant_type=client_credentials"
Connect with the token
Most automated setups use mcp-remote with a static Bearer header. Request a token from /v1/token, then pass it when connecting:
npx -y mcp-remote "https://mcp.port.io/v1" \
--header "Authorization: Bearer YOUR_ACCESS_TOKEN" \
--header "x-read-only-mode: 0"
If your MCP client supports OAuth machine-to-machine authentication natively, point it at the MCP server URL and let it handle token exchange against /v1/token using your Port client ID and secret.
Access tokens expire (typically about 3 hours). Request a new token from /v1/token before expiry, or on 401 responses. Do not treat the access token itself as a long-lived secret.
The client_credentials grant is also rate-limited to 50 requests per 15 minutes per client. Cache tokens until they are close to expiry instead of requesting a new token on every MCP call.
Example: Claude Code in GitHub Actions
Here is a complete example showing how to connect Claude Code to the Port MCP server within a GitHub Actions workflow:
Show example workflow
name: Port MCP Server Demo with Claude Code
on: workflow_dispatch
env:
PORT_MCP_URL: ${{ vars.PORT_MCP_URL }}
jobs:
demo:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Authenticate with Port
id: port-auth
run: |
response=$(curl -s -X POST "${{ env.PORT_MCP_URL }}/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=${{ secrets.PORT_CLIENT_ID }}" \
-d "client_secret=${{ secrets.PORT_CLIENT_SECRET }}")
token=$(echo "$response" | jq -r '.access_token')
echo "::add-mask::$token"
echo "access_token=$token" >> "$GITHUB_OUTPUT"
- name: Claude Code against Port MCP
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
mode: agent
mcp_config: |
{
"mcpServers": {
"port-prod": {
"command": "npx",
"args": [
"mcp-remote",
"${{ env.PORT_MCP_URL }}",
"--header",
"Authorization: Bearer ${{ steps.port-auth.outputs.access_token }}",
"--header",
"x-read-only-mode: 0"
]
}
}
}
allowed_tools: "mcp__port-prod__list_blueprints,mcp__port-prod__get_entities"
direct_prompt: |
List all blueprints, then show entities of the "zendesk_ticket" blueprint.
How this workflow works
- Authentication - The
port-authstep exchanges your Port client credentials for a short-lived access token viaPOST /v1/tokenwithgrant_type=client_credentials. - MCP connection - Claude Code connects to the remote MCP server using the
mcp-remotepackage, passing the access token in theAuthorizationheader. - Read-only mode - The
x-read-only-modeheader defaults to0, which allows all tools based on your permissions. You can change it to1to restrict the MCP server to only expose read-only tools, completely hiding write tools from the available tools list. - Action filtering - The
x-allowed-actions-to-runheader (optional) allows you to control which actions are available through therun_actiontool. It accepts a comma-separated list of action identifiers. For example,x-allowed-actions-to-run: "create_github_issue,create_incident"restricts access to only the specified actions. - Tool access - Claude Code can invoke only the specific Port tools listed in
allowed_tools, ensuring controlled access to your Port instance. - Execution - The AI agent executes the provided prompt using the available Port tools to query your software catalog.
For read-only workflows, you can change the x-read-only-mode header from 0 to 1 to restrict tools to read-only operations, or limit allowed_tools to just the query operations you need.
To control which actions are available, use the x-allowed-actions-to-run header with a comma-separated list of action identifiers. For example:
x-allowed-actions-to-run: "create_github_issue,merge_github_pr"- Only the specified GitHub actions.x-allowed-actions-to-run: "create_incident,send_notification"- Only incident and notification actions.
Choose the appropriate MCP URL for your Port region (EU: https://mcp.port.io/v1, US: https://mcp.us.port.io/v1).
Adapting for other AI agents
While this example focuses on Claude Code, the same authentication pattern can be applied to other AI agents and platforms:
- Obtain Port credentials - Create a client ID and secret in your Port dashboard.
- Generate access token - Request a token from
POST /v1/tokenwithgrant_type=client_credentials. - Configure MCP connection - Point your AI agent to the remote MCP server with the token.
- Define tool permissions - Specify which Port tools the AI agent can access.
- Execute workflows - Let the AI agent interact with your Port data and capabilities.
When to use which method
- Local IDE with browser login - Use interactive OAuth from the installation guide.
- CI/CD or headless agents - Request a token from
POST /v1/tokenwithgrant_type=client_credentials, then pass it as a Bearer token. - MCP clients with native OAuth M2M support - Configure the client to use the MCP server OAuth metadata so it handles token lifecycle automatically.
Port API compatibility
If you already obtain a machine token via the Port REST API (POST /v1/auth/access_token), you can use that same Bearer token with the MCP server. The MCP token endpoint returns the same JWT under the hood.
This is useful when a pipeline already authenticates against the Port API for other calls and you want to reuse the same token pattern. For new MCP integrations, prefer POST /v1/token so your auth flow stays within the MCP OAuth surface and supports discovery via .well-known/oauth-authorization-server.
Security best practices
- Use a dedicated automation credential with the minimum permissions needed.
- Rotate client secrets if they are exposed.
- Mask tokens in CI logs (for example,
::add-mask::in GitHub Actions). - Prefer
x-read-only-mode: 1for read-only automation. - Use
x-allowed-actions-to-runto restrict which actionsrun_actioncan trigger.