Portcall
A plugin gateway that serves local MCP servers over HTTP as independent endpoints, avoiding child-process leaks by calling server factories in-process.
README
Portcall
A small plugin gateway that serves local MCP servers over HTTP.
The name is a nautical pun: port call — port (harbour / network port) + call
(a ship's stop / a request).
What it is
Portcall listens on one HTTP port and mounts one or more MCP servers at separate paths:
/vault/mcp → mcpvault (Obsidian vault)
/healthz → liveness + mount list
Each mount is an independent MCP endpoint. Clients register them separately — there is no tool aggregation, so no name collisions and no namespacing scheme to maintain.
Exposing the port beyond localhost is deliberately out of scope. Put a tunnel,
reverse proxy, or nothing at all in front of it; Portcall binds to 127.0.0.1
by default and does not care what is upstream.
Why not a stdio bridge
The obvious way to put a stdio MCP server on HTTP is a generic bridge such as
supergateway. That works, but it has a structural problem: every request or
session spawns a child process, and reaping those children is easy to get wrong.
In supergateway specifically, the child is only reclaimed from
transport.onclose or transport.onerror. Nothing calls transport.close() on
a normally-completed stateless request, so onclose never fires and every
successful request leaks a process — only failed requests get cleaned up. Its
stateful mode does not leak (a session timer closes the transport), but it holds
a long-lived GET SSE stream instead, which some proxies handle badly.
Wrapping the command in npx makes it worse: npx forks the real server, so
killing the child kills the wrapper and orphans the grandchild.
Portcall's answer is to not spawn anything when it does not have to.
Adapters
| Adapter | For | How |
|---|---|---|
inProcess |
Servers that export a factory as a library | Calls the factory in-process. No child process exists, so there is nothing to reap. |
stdio |
Third-party servers that only speak stdio | Not implemented yet. When it lands it must reclaim the child on normal completion, not just on error, and handle process-group kills for wrapper commands. |
inProcess is the interesting case and covers the servers worth self-hosting.
@bitbonsai/mcpvault, for example, exports createServer(vaultPath, options)
returning an MCP SDK v2 Server; its bin entry is essentially
serveStdio(() => createServer(...)). Portcall calls the same function directly
and skips stdio entirely.
The SDK builds a fresh server instance per request and disposes it with the request, so there is no session state to time out and no accumulating handles.
Protocol versions
Portcall is built on @modelcontextprotocol/server v2, which serves two
protocol eras from a single handler:
- Modern (
2026-07-28) — per-request envelope. Requests carryMCP-Protocol-Version,Mcp-Method, and (for tool calls)Mcp-Nameheaders plus aparams._metablock. There is noinitializehandshake and no long-lived session; discovery isserver/discover. - Legacy (2025-era) — served statelessly by default.
GETandDELETE(2025 session operations) answer405. SetPORTCALL_MODERN_ONLY=trueto reject legacy traffic outright.
Because the modern era is per-request, there is no standing SSE stream to keep
open. That sidesteps a class of proxy problem: some reverse proxies withhold
response headers until the first body byte arrives, which stalls a
just-opened-but-silent SSE stream indefinitely. For the streams that do occur,
PORTCALL_KEEPALIVE_MS controls the SSE comment-frame interval; lower it if a
proxy in front is buffering.
Configuration
All host-specific values come from the environment.
| Variable | Default | Meaning |
|---|---|---|
PORTCALL_VAULT_PATH |
(required) | Absolute path to the Obsidian vault to serve |
PORTCALL_PORT |
7100 |
TCP port |
PORTCALL_HOST |
127.0.0.1 |
Bind interface |
PORTCALL_TOKEN |
(unset) | Static bearer token. Unset means no authentication |
PORTCALL_ALIAS_ROOT_MCP |
(unset) | Also mount the named plugin at /mcp |
PORTCALL_PATH_PREFIX |
(unset) | Serve every mount under /<prefix>/… |
PORTCALL_KEEPALIVE_MS |
15000 |
SSE keepalive interval; 0 disables |
PORTCALL_MODERN_ONLY |
false |
Reject 2025-era requests instead of serving them |
PORTCALL_TOKEN gates every mount with Authorization: Bearer <token>. Note
that some MCP clients — Claude's custom connector UI among them — offer no way
to set a request header, so for those the token has to be enforced upstream
instead (or left off, with access controlled at the network layer).
PORTCALL_PATH_PREFIX is the fallback for exactly those clients: it moves every
mount under a segment you choose, so /vault/mcp becomes /<prefix>/vault/mcp
and the URL itself carries the secret. Two things follow from that, and the
server enforces both:
404responses say onlynot_found. They never list what is mounted.- The mount listing moves out of the public
/healthzand into/<prefix>/healthz. The bare/healthzstill answers, so liveness probes keep working, but it discloses no paths.
Treat a path prefix as weaker than a header. URLs reach proxy access logs, crash reports, and anything that records a destination, and a leaked one grants the same access a leaked token would. It raises the bar — it is not authentication.
Which plugins are mounted, and where, is declared in plugins.config.ts.
Running
Requires Node 24 (see .nvmrc).
npm install
npm run build
cp .env.example .env # then set PORTCALL_VAULT_PATH
npm start
Both npm start and npm run dev load .env if it is present and start
without it if it is not, so a daemon can inject the environment directly
instead. Variables already set in the environment are not overridden.
npm run dev runs the entry point through tsx with watch. A daemon should
run the built output, not tsx.
Check it is up:
curl -s localhost:7100/healthz
Tests
npm test # builds, then runs unit and integration tests
npm run typecheck
No test dependencies: the runner is node:test, and tsx (already needed for
npm run dev) loads the TypeScript.
The integration tests are black-box. They spawn the built server against a
throwaway vault on an ephemeral port and drive it over real HTTP, so they
exercise the same artifact a daemon runs — routing, the /mcp alias, bearer
auth, and both protocol eras. The unit tests cover mount resolution and the
bearer check, where a silent regression would look like a dead client rather
than an error.
Layout
src/
server.ts HTTP entry point, wiring, health, shutdown
routes.ts mount resolution and URL normalisation
auth.ts bearer token check
config.ts environment parsing
log.ts structured logging
types.ts the Plugin interface
adapters/
inProcess.ts library-factory adapter
plugins/
vault.ts mcpvault
plugins.config.ts which plugins mount at which paths
test/
integration.test.ts black-box tests against the built server
routes.test.ts mount resolution
auth.test.ts bearer token check
helpers.ts server harness and MCP request builders
License
MIT
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
E2B
Using MCP to run code via e2b.