portal-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@portal-mcp log in to my internet provider's portal and list my latest invoices"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
portal-mcp
Turn any website or customer portal into MCP tools, without an open API and without writing code. portal-mcp is a Claude Code plugin and a local MCP server. You point it at a site, log in yourself if the site needs it, and the plugin explores the pages in a real browser, learns how they load their data, and lets you choose which tools Claude gets: list your invoices, download a PDF, look something up, fill in a form.
Works with logged-in services, customer portals and public sites.
You log in yourself, in a visible browser window. The runtime never reads or stores your password.
Each service is described by a YAML manifest. The runtime turns the manifest into MCP tools.
Sessions are stored encrypted. The encryption key lives in your system keyring.
Write actions run in two steps and only after you approve them yourself, in a dialog or a local confirmation page. The model cannot approve its own actions.
Every write action is logged in a local audit log.
Status: version 0.9.7. Runtime with read, file, write and binding tools and human approval; logins through other hosts, BankID and OpenID Connect apps; data hosts called with the app's own token; HTML pages as read tools; file uploads; public sites without login; encrypted sessions and several profiles per service; a recorder that explores pages by itself and captures writes without sending them; self_test, desktop notifications, the portal-mcp-creator skill and the slash commands. Tested against a local mock service.
Requirements
A computer with a graphical desktop: macOS, Windows, or Linux with a desktop environment.
uvto run the runtime.Google Chrome (preferred), or Playwright's Chromium:
uv run playwright install chromium.
Related MCP server: guarded-browser-mcp
Install
Pick the way that matches where you use Claude. Everything except the last option installs the whole plugin: the runtime, the portal-mcp-creator skill and the /portal-mcp:* commands.
Where you use Claude | Way to install |
Claude Code in a terminal | |
The Claude desktop app, Code tab | |
Claude Code in VS Code or JetBrains | |
A team or a project | |
Working on the plugin itself | |
Another MCP client, or the desktop app's chat | The runtime alone (tools only) |
While the repository is private, every way fetches it with your local git credentials, so only accounts with access can install it. With the GitHub CLI: gh auth login, then gh auth setup-git.
Claude Code in a terminal
/plugin marketplace add markstrom/portal-mcp
/plugin install portal-mcp@portal-mcp/plugin opens an interactive dialog, so it works only in Claude Code in a terminal.
The command line
Run these in any terminal. Claude Code in a terminal, the desktop app and the IDE extensions share the same installation, so this works for all of them:
claude plugin marketplace add markstrom/portal-mcp
claude plugin install portal-mcp@portal-mcpRestart the app or Claude Code afterwards so the plugin is loaded.
The Claude desktop app, Code tab
The Code tab cannot run /plugin. Either use the command line and restart the app, or add the marketplace markstrom/portal-mcp in the app's plugin settings and install portal-mcp from there.
A team or a project
Add this to .claude/settings.json in a project. Everyone who opens the project in Claude Code and trusts it is offered the plugin:
{
"extraKnownMarketplaces": {
"portal-mcp": { "source": { "source": "github", "repo": "markstrom/portal-mcp" } }
},
"enabledPlugins": { "portal-mcp@portal-mcp": true }
}A local clone, for development
git clone https://github.com/markstrom/portal-mcp.git
claude plugin marketplace add ./portal-mcp
claude plugin install portal-mcp@portal-mcpOr load it for one session only, without installing: claude --plugin-dir ./portal-mcp.
The runtime alone, as an MCP server
Other MCP clients can use the runtime directly. You then get the tools of the services you have already added, but not the skill or the /portal-mcp:* commands, so new services are added from Claude Code. Clone the repository first, then point the client at it.
For Claude Code without the plugin:
claude mcp add portal-mcp -- uv run --quiet --project /path/to/portal-mcp portal-mcp-runtimeFor the Claude desktop app's chat, or any client that reads an mcpServers file (in the desktop app: Settings, Developer, Edit config):
{
"mcpServers": {
"portal-mcp": {
"command": "uv",
"args": ["run", "--quiet", "--project", "/path/to/portal-mcp", "portal-mcp-runtime"]
}
}
}Clients that connect over HTTP can use uv run portal-mcp-runtime --http and the address http://127.0.0.1:8765/mcp instead.
Check, update and remove
Check:
/portal-mcp:listshows the loaded services. If the tools do not show up, restart the app or Claude Code.Update:
claude plugin update portal-mcp@portal-mcp, then restart. In a terminal session,/pluginhas the same option.Remove:
claude plugin uninstall portal-mcp@portal-mcpandclaude plugin marketplace remove portal-mcp. Your services, sessions and downloads stay in the data directory (see Where your data lives) until you delete them.
Commands
Command | What it does |
| Add a service, logged in or public. It starts at once: you log in if the service needs it, the plugin visits and explores the pages itself, then shows a catalog where you choose the tools. |
| Explore more parts of a service, or redo discovery for tools that stopped working. |
| Services, profiles, tools and session status. |
| Run |
| Log out at the service and delete the local session. |
How it fits together
flowchart LR
client["MCP client<br/>(Claude Code)"] -- "stdio or localhost HTTP" --> runtime["portal-mcp runtime"]
runtime --> manifests["services/*.yaml"]
runtime --> store["sessions/ (encrypted)"]
runtime -- "HTTP with the session" --> service["The service's web API"]
runtime -- "opens and watches" --> browser["Visible browser window<br/>(you log in here)"]
creator["portal-mcp-creator skill"] -- "writes manifests" --> manifests
creator -- "guided recording" --> browserThe skill creates manifests. The runtime turns them into tools. The browser is only used for logging in and recording, and you control the login.
Public sites work too: with session: {type: none} there is no login, no session and nothing stored, and tools call the site without cookies. If a site has both public pages and a login, /portal-mcp:add asks once which part you want. For public sites the skill suggests a low rate limit and caching of reads.
While recording, the plugin explores each page itself: it clicks tabs, expandable sections and "show more" buttons, because many single-page apps load their data only after such a click. It never clicks controls that mean an action (log out, delete, pay, send, save, confirm and similar), submit buttons, unlabeled controls or downloads, and every write a click causes is blocked and captured, never sent.
Login and a tool call
sequenceDiagram
participant C as MCP client
participant R as Runtime
participant K as Session store
participant B as Browser window
participant S as Service
C->>R: call example_list_invoices
R->>K: load session
alt no valid session
R->>B: open login_url, notify the user
B->>S: user logs in
R->>S: poll session check until 200
R->>K: save encrypted session
R-->>C: "log in and call again" (if not done within ~20 s)
end
R->>S: GET /api/invoices with session
S-->>R: JSON
R->>R: pick fields, redact, cache
R-->>C: resultCreating a service (skill pipeline)
flowchart TD
A[Scope: which service and functions] --> B[Recon without login]
B --> C[Guided recording<br/>you log in, GET only after login]
C --> D[Analysis: path templates, fields, pagination]
C --> W[Forms submitted by you<br/>writes captured, not sent]
W --> D
D --> E[Catalog: read / file / write / binding, flags]
E --> F{You choose tools}
F --> G[Generate manifest, you approve]
G --> H[Verify tools against recording]
H --> I[Delete recordings, commit manifest]Write actions and approval
sequenceDiagram
participant M as Model (via MCP client)
participant R as Runtime
participant U as You
participant S as Service
M->>R: example_prepare_pay_invoice(amount, invoice_id)
R->>R: check schema and limits
R-->>M: summary + one-time code (5 min)
M->>R: example_confirm_pay_invoice(code)
alt client supports elicitation
R->>U: dialog in the client with the summary
else
R->>U: local confirmation page in the browser<br/>(secret address, never shown to the model)
end
U-->>R: Bekräfta / Avbryt
alt approved
R->>S: POST with the session
S-->>R: response
R->>R: audit log: approved, executed
R-->>M: done
else declined or no answer
R-->>M: declined, or "waiting for approval, call again"
endThe summary you approve is built by the runtime from the real values, not written by the model. Codes are one-time and expire after five minutes. Limits are checked when the action is prepared and again when it is confirmed.
Running it somewhere else
By default the runtime runs on your own computer, started by Claude Code over stdio, with no network connection. It can also run on another computer of yours that has a desktop, reached through HTTP mode and an SSH tunnel; you then log in through screen sharing. A server without a desktop cannot show the login window, so only public sites can work there. HTTP mode has no authentication, so it must listen on 127.0.0.1 only.
See docs/remote.md for the steps, the security rules and the known limits.
Where your data lives
Data is kept outside the plugin, so updates never remove it. The location follows your OS conventions (platformdirs):
<user data dir>/portal-mcp/
services/<service>.yaml manifests
services/<service>.schemas.json self_test baselines (structure only)
profiles/<service>/<profile>/ browser profiles
sessions/ encrypted sessions
recordings/ temporary recordings, deleted after a manifest is written
uploads/ files that upload tools may send (nothing else can be uploaded)
audit.log every write action: time, service, tool, call and responseSet PORTAL_MCP_HOME to use another base directory. Downloaded files go to your Downloads folder, or to PORTAL_MCP_DOWNLOADS if set.
Manifest
One file per service, services/<name>.yaml. The file name must match name. A broken manifest only affects its own service.
The runtime watches services/ and sends tools/list_changed when a manifest changes. Some clients still need a restart before new or changed tools show up.
name: example
enabled: true
base_url: https://app.example.com
login_url: https://app.example.com/login
profiles: [default]
session:
type: cookies # cookies | bearer
bearer_source: null # for bearer: "localStorage:<key>" or "sessionStorage:<key>"
csrf: # optional
header: x-csrf-token # send the token in this header, and/or
body_field: null # put it in this body field
from_cookie: csrf # take it from this cookie, or
from_page: null # from a page: {path: /form, name: <hidden input or meta name>}
check: /api/session # 200 = logged in, 401 = not logged in
logout: /api/logout # optional, called with POST
login_hosts: [login.example.com] # optional: other hosts the login goes through
data_hosts: [api.example.net] # optional: APIs on other hosts the pages load data from
asset_hosts: [cdn.example.net] # optional: CDNs for scripts, styles, fonts and images
data_auth: # optional: the token tools on data hosts send
source: "sessionStorage:oidc.user:*" # where the service's own app keeps it ("*" matches anything)
field: access_token # a field of a JSON value, if the value is JSON
page: /cases # a page that makes the app get its token
login_hosts_after_login: blocked # or oidc: login host open after login for OIDC endpoints only
rate_limit_per_minute: 30
cache_seconds: 30 # caches read results
redact: [items.owner.national_id]
tools:
- name: list_invoices
kind: read # read | file | write | binding
description: List invoices with date, amount and status.
method: GET
path: /api/invoices
params:
type: object
properties:
page: {type: integer}
pick: [items.id, items.date, items.amount, items.status]
- name: download_invoice_pdf
kind: file
description: Download the PDF for one invoice.
method: GET
path: /api/invoices/{invoice_id}/pdf
params:
type: object
properties:
invoice_id: {type: string}
required: [invoice_id]
- name: pay_invoice
kind: binding # binding: payments, bids, orders, signatures
description: Pay an invoice.
method: POST
path: /api/invoices/{invoice_id}/pay
params:
type: object
properties:
invoice_id: {type: string}
required: [invoice_id]
body_encoding: json # json | form
body_schema:
type: object
properties:
amount: {type: number}
required: [amount]
body_constants: {} # fixed body fields, for example hidden form inputs
limits: # required for binding tools
max: {amount: 5000} # also: min, allowed (list of values), max_length
summary: "Pay {amount} for invoice {invoice_id}"
- name: send_message
kind: write
messaging: true # sends free text to other people: off by default
enabled: true # needed to turn a messaging tool on
method: POST
path: /api/messages
body_schema:
type: object
properties:
to: {type: string}
text: {type: string}
required: [to, text]Read and file tools are prefixed with the service name, for example example_list_invoices. Write and binding tools become example_prepare_pay_invoice and example_confirm_pay_invoice. Every tool except confirm_ also takes an optional profile argument.
Common tools:
list_services: services, profiles, session status, tools, hidden tools, load errors and the data paths.forget_session(service, profile?): delete the local session without contacting the service.logout(service, profile?): call the logout endpoint and delete the local session; without a profile, all profiles.self_test(service, profile?, update_baseline?): run the read tools and compare each answer's structure with the saved baseline.
Self test
self_test runs every exposed read tool. Tools with required parameters need test_args in the manifest, for example test_args: {page: 1}; without them they are skipped. Write and file tools are never run.
The first run saves the structure of each answer (field names and types, never values) in services/<service>.schemas.json. Later runs report removed or changed fields as changed, and new fields as notes. Run it with update_baseline: true to accept a new structure.
An answer that is empty, or whose items are all empty after pick, gets status empty and is never saved as a baseline; unmatched_pick lists pick paths that matched no item. When a list's items come in more than one shape, the saved structure keeps one variant per shape.
Pick paths can have fallbacks: item.name|name uses the first path that exists in each item and writes the value at the first path, so items of different shapes come out in one shape.
Profiles
A service can have several profiles, for example two people in a household or a private and a business account: profiles: [default, business]. Each profile has its own browser profile and its own encrypted session. Every tool takes an optional profile argument; the first call with a new profile opens a login window for it.
Notifications
The runtime shows a desktop notification when a login is waiting, when a login is done, and when a confirmation page is waiting for you. macOS uses osascript, Linux uses notify-send when it is installed, and Windows uses desktop-notifier. Set PORTAL_MCP_NO_NOTIFY=1 to turn them off.
Logging in
When the creator skill records a service, every recording starts with a clean, temporary browser profile, checks that the login is really verified (only a 200 from session.check counts, and a check that answers 200 without a login is refused), and only then lists the service's menu links for you to choose from. If a service rejects a request because its headers are too large (usually too many cookies), the plugin says so, shows the cookies for the service's host by name and count only, and suggests a clean profile; forget_session with browser_profile: true deletes the runtime's browser profile.
Some pages are single-page apps that run their own OpenID Connect client after the login: they load a library from a CDN and get their own token from the login host. Declare the CDN in asset_hosts (static files only: scripts, styles, fonts, images) and set login_hosts_after_login: oidc to keep the login host open after login for the endpoints its discovery document names (POST only to the token endpoint, never the logout endpoint). Both are off by default. The analyzer refuses recordings whose login was not verified, unless --allow-unverified is given.
Some services load their data from an API on another host. List such hosts in data_hosts. When recording, a data host is allowed for GET over https, and writes to it are captured, not sent, just like the service's own host. It never gets the service's cookies. Tools can call a data host when the service's own app authenticates to it with a token: data_auth says where the app keeps that token after login, the runtime opens data_auth.page after you log in, reads the token, stores it encrypted with the session, and sends it as Authorization: Bearer only to declared data hosts, for tools with host set. The recording reports which storage keys hold tokens (key names only) so the skill can fill this in.
HTML pages work as read tools too: response: html with embedded reads JSON that the page carries in a <script> tag (for example script#__NEXT_DATA__ or JSON-LD), and extract takes values with CSS selectors (text, an attribute, lists, and one object per repeated element). The page is parsed as data: no script runs and nothing else is fetched, and only text, attribute values and the selected JSON are returned. The analyzer describes HTML pages with a skeleton that holds no text.
Other tool options: public: true makes a read or file tool call the site without the session and never open a login, for public pages in a service that otherwise needs one. body_encoding: multipart with file_fields uploads files; a file argument must be a file in the uploads folder of the data directory, and the approval shows each file's name and size. File tools may download up to 200 MB; other answers are limited to 10 MB.
While recording, consent managers (cookie banners) may load with GET, so pages that wait for them show their content, and exploring declines the banner; analytics and advertising stay blocked.
Many services log in through another host, for example an identity provider or an account service on its own address. List such hosts in login_hosts, as plain host names. When the creator skill records a service, a login host is allowed only while you log in, only over https, and is blocked after login. The saved session only ever holds cookies for the base_url host. Hosts are never added automatically: if a recording is stopped by an unknown host, the skill asks you whether you trust it.
When a tool needs a session and none is valid, a browser window opens on login_url and a desktop notification asks you to log in. The runtime checks the session check endpoint and captures the session once you are logged in. A tool call waits up to about 20 seconds; if you are not done by then, it tells you to finish in the window and call again. The login continues in the background. Only one login runs at a time.
Development
uv sync
uv run pytestThe tests run against tests/mock_service/, a local fake portal with invented data. Tests that need a browser are skipped if neither Google Chrome nor Playwright's Chromium is installed.
Run the server directly:
uv run portal-mcp-runtime # stdio (default)
uv run portal-mcp-runtime --http # streamable HTTP at http://127.0.0.1:8765/mcpLogs go to stderr only.
Security notes
Passwords are never handled by this software. Logging in happens in a window you can see.
The browser always runs with Chrome's sandbox on. If it cannot start with the sandbox, the login fails with an error instead of starting without it.
Session files are encrypted with a key stored in the system keyring and have mode 0600.
The HTTP transport only binds to localhost.
Read results can be trimmed with
pickandredactbefore they reach the model.Write actions need a person's approval through MCP elicitation or a local confirmation page. The page is served on 127.0.0.1, its address carries a secret token that the model never sees, and it only accepts clicks from its own origin.
Tools that send free text to other people are off unless the manifest turns them on.
During recording, writes are stopped in the browser and only their format is saved, unless you explicitly agree to a real action.
Set
PORTAL_MCP_APPROVAL=browserto always use the confirmation page, even if the client supports elicitation.
License
Apache License 2.0. See LICENSE.
Available Tools
4 toolsforget_sessionA
Delete the local session for a service without contacting the service. With browser_profile, also delete the browser profile (cookies and device memory) so the next login starts clean; the service may then ask to recognize the device again.
Inputs: service (required), profile, browser_profile.
list_servicesARead-only
List loaded services, their profiles, session status, tools and load errors.
Inputs: no named parameters.
logoutA
Log out of a service (calls its logout endpoint if configured) and delete the local session. Without a profile, all profiles of the service are logged out.
Inputs: service (required), profile.
self_testB
Run the read tools of a service and compare the structure of each answer with the structure saved when the tool was verified. Saves a baseline for tools that have none. Write and file tools are not run. Set update_baseline to accept the current structure as the new baseline.
Inputs: service (required), profile, update_baseline.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 4 tools
Each tool targets a distinct action: listing state, deleting a local session, logging out remotely plus clearing local state, and verifying tool output structures. forget_session and logout are the closest pair, but the descriptions explicitly contrast them (local-only vs. contacting the service's logout endpoint), so misselection risk is low.
All names are snake_case and mostly follow a verb_noun pattern (list_services, forget_session, self_test). 'logout' is a bare verb and 'self_test' is a noun-like compound, but the style is uniform enough to be predictable.
Four tools is a small but well-scoped surface for a session/service-management server, with no redundant members. It leans slightly thin, since a handful of related operations (login, reload, profile inspection) could plausibly belong here, but nothing feels padded.
The surface covers observing services, tearing down sessions, and validating tool contracts, but there is no operation to authenticate/start a session, reload a service after config or load errors, or inspect individual tool results beyond the self_test summary. Agents will often need to work around these gaps indirectly through the service's own tools.
Maintenance
Related MCP Connectors
Let your AI agent operate any website through a real, logged-in browser session.
Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.
Whole browser tasks done in pilotfish's cloud for any MCP agent, signed in with your saved logins.
Let AI agents use any website you're logged in to. Sessions stay encrypted on your machine.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceMCP server that lets agents drive your real Chrome browser with existing logins and sessions via an outbound-only WebSocket extension. It exposes Playwright-compatible browser tools for navigation, clicking, typing, and snapshots.1Apache 2.0
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to drive a self-hosted Chromium through MCP tools for opening pages, reading content, taking screenshots, clicking, filling forms, and triggering final submissions, while a human watches the same browser live and can take over for logins or captchas. Server-side guardrails enforce per-session domain allowlists and denylists, refuse password, payment card, IBAN and one-time-code fields, isolate each token in its own browser context, and route submits, payments and orders through a separate final-action tool that can require human approval.MIT
- AlicenseNot gradedqualityBmaintenanceEnables any agent to drive a dedicated Chromium instance through MCP, including opening and reading pages, taking steps, logging in via a password vault, downloading/uploading files, and replaying saved routines. It also gates sensitive actions behind approvals and can use client certificates or one-time card data for official procedures.MIT
- AlicenseNot gradedqualityBmaintenanceEnables any MCP-capable AI agent to operate the user's own everyday Chrome browser with all existing login sessions intact, offering 22 tools for reading page/network data, filling forms, clicking, batch multi-step actions, and handing CAPTCHAs or confirmations back to the human — without API keys, debug ports, or launch flags.MIT