Skip to main content
Glama
wva2ccyk-prog

personal-mcp-bridge

personal-mcp-bridge

A minimal, read-only bridge that lets an MCP client (or any HTTP caller) browse a few allowlisted local directories safely. Every tool is read-only; there is no write, edit, delete, or execute path anywhere in the surface.

Tools are available through the MCP endpoint POST /mcp, the localhost-only compatibility dispatcher POST /call, and dedicated HTTP endpoints where noted:

  • list_roots / GET /roots - list the directories you allowlisted, by alias

  • list_files / GET /files - list one allowlisted directory

  • read_file / GET /read - read one bounded file inside an allowlisted root

  • read_file_range / GET /read-range - read a bounded line range

  • tail_file / GET /tail - read the end of a bounded file

  • read_multiple_files - read several bounded files through MCP or /call

  • get_file_info / GET /file-info - inspect one allowlisted path's metadata

  • search / GET /search - search text files under an allowlisted root

It is intentionally small.

read_file, read_multiple_files, and search also understand PDF/DOCX/PPTX/XLSX, citation-prefixed (e.g. [p.3], [Sheet1!2-5]), when you install the optional documents extra (see below). Still read-only, and it degrades gracefully: without the extra, those formats are simply not there.

Read output is character-bounded: read_file/search accept an optional max_chars (capped by BRIDGE_READ_MAX_CHARS) and report the applied limits.

What this is not

This is not a full personal automation layer. It does not run agents, write files, run shell commands, drive a browser, or keep any memory, audit, or cache database. It is an alpha, read-only file bridge with safe defaults. If you came looking for a do-everything assistant runtime, this is the deliberately boring, auditable subset.

Related MCP server: Local Files MCP Server

Safety model in one paragraph

Fail closed. With no roots configured, every request is blocked. Paths are relative-only with no traversal, no drive letters, and no symlink escapes. Common secret, runtime, cache, and generated paths (.env, token/secret/ credential-like files, runtime/settings.json, .git, virtualenvs, node_modules, caches) are excluded from listing, reads, and search. In public/tunnel mode the bridge refuses to start without a strong token, refuses tokens passed in the URL, refuses the generic /call endpoint for any forwarded/remote request, and never emits an absolute local path. See SECURITY.md and THREAT_MODEL.md.

Install

Requires Python 3.10+.

python -m pip install -e .
# or, just install the runtime deps:
python -m pip install starlette uvicorn

To also read PDF/DOCX/PPTX/XLSX files, install the optional documents extra (pypdf, python-docx, python-pptx, openpyxl). The base install stays tiny without it:

python -m pip install -e ".[documents]"

Run the demo (no real files touched)

The demo allowlists only the bundled synthetic demo/mock_data directory and exercises all three tools in-process:

python demo/run_demo.py

You will see list_roots, a file read, a search hit on mock data, and a traversal attempt being refused.

Run the server against your own files

Allowlist one or more directories, then start the bridge on loopback:

export BRIDGE_ROOTS="notes=/path/to/notes;docs=/path/to/work/docs"
python -m personal_mcp_bridge
# serving on http://127.0.0.1:8787

Then:

curl http://127.0.0.1:8787/roots
curl "http://127.0.0.1:8787/files?root=notes&path=."
curl "http://127.0.0.1:8787/read?root=notes&path=welcome.md&max_chars=4000"
curl "http://127.0.0.1:8787/read-range?root=notes&path=welcome.md&start_line=1&line_count=20"
curl "http://127.0.0.1:8787/tail?root=notes&path=welcome.md&last_lines=20"
curl "http://127.0.0.1:8787/file-info?root=notes&path=welcome.md"
curl "http://127.0.0.1:8787/search?root=notes&q=budget&max_chars=4000"

On loopback with no token set, local calls are allowed for convenience. To require a token even locally, set BRIDGE_TOKEN and send Authorization: Bearer <token>.

Runtime limits

Read output uses character caps for schema-facing text; request bodies and search-file scanning use byte caps. Restart the bridge and reconnect/re-register the MCP connector after changing any of these, since some clients cache schemas.

Variable

Default

Purpose

BRIDGE_READ_MAX_CHARS

128000

Maximum returned characters for read/search tools.

BRIDGE_SEARCH_MAX_FILE_BYTES

1048576

Largest file searched/read by helpers that scan full files.

BRIDGE_MAX_MATCHES

200

Maximum search matches before truncation.

BRIDGE_HTTP_MAX_BODY_BYTES

1000000

Maximum accepted JSON request body size.

BRIDGE_DOC_MAX_FILE_BYTES

26214400

Largest PDF/DOCX/PPTX/XLSX accepted for parsing.

BRIDGE_XLSX_MAX_SHEETS

20

Sheets read per workbook.

BRIDGE_XLSX_MAX_ROWS

2000

Rows read per sheet.

BRIDGE_XLSX_MAX_COLS

50

Columns read per row.

BRIDGE_LIST_FILES_MAX_ENTRIES

1000

Maximum entries returned by list_files; results beyond this are truncated.

BRIDGE_RATE_LIMIT_PER_MINUTE

0 (disabled)

Per-peer HTTP rate limit. In public/tunnel mode a finite default (600/min) applies if unset; setting it to 0 is refused at startup.

BRIDGE_DOC_MAX_ARCHIVE_MEMBERS

1024

OOXML preflight: maximum member count.

BRIDGE_DOC_MAX_UNCOMPRESSED_BYTES

134217728

OOXML preflight: maximum total uncompressed size.

BRIDGE_DOC_MAX_MEMBER_BYTES

33554432

OOXML preflight: maximum single-member uncompressed size.

BRIDGE_DOC_MAX_COMPRESSION_RATIO

100

OOXML preflight: maximum compression ratio per member.

Compatibility aliases BRIDGE_MAX_READ_CHARS, BRIDGE_MAX_READ_BYTES, and BRIDGE_MAX_FILE_BYTES are still accepted for existing setups.

read_file streams only the bytes it will return, and read_file_range and tail_file stream a bounded window from a fixed-size chunk reader, so they work on files of any size, including files that are one enormous line (or have no newlines at all), without scaling peak memory to the file size. Requests bodies are streamed and bounded too: consumption stops with 413 the moment more than BRIDGE_HTTP_MAX_BODY_BYTES has been received, and Content-Length is not trusted as the only enforcement. Documents are the exception at the parse layer: their third-party libraries read whole files, so oversized ones are refused before parsing (BRIDGE_DOC_MAX_FILE_BYTES).

HTTP and error behavior

  • POST /mcp returns 200 for JSON-RPC responses and 202 Accepted with no body for valid notifications (a well-formed envelope with no id), as the MCP Streamable HTTP transport requires. A malformed object is not a notification: it receives a JSON-RPC error response (-32600/-32602), and an empty HTTP body returns 400 invalid-json. A notifications/* method carrying an id is an error (-32600), never a silent 202.

  • Error responses use stable codes, grouped into categories, rather than raw exception text:

    • Tool execution (including read_multiple_files per-file results): path-excluded, file-not-found, not-a-directory, invalid-argument, permission-denied, unknown-root, archive-rejected, internal-error.

    • HTTP transport / input: invalid-json, invalid-content-length, request-body-too-large, json-object-required, unexpected-json-rpc-response.

    • Authentication / exposure: token-in-url-refused, invalid-token, host-not-allowed, origin-not-allowed, generic /call is localhost-only.

    • Rate limit: rate-limited (HTTP 429 with Retry-After). POST /call uses invalid-argument for invalid envelopes and unknown-tool for unknown tools. Unexpected failures are logged with the exception type, stack frames, and an incident id (never the raw exception message, which may embed tokens or paths) and surfaced only as internal-error, so an OS-level exception can never leak an absolute local path.

  • JSON bodies are parsed strictly: NaN/Infinity/-Infinity are rejected and parser errors map to 400 invalid-json (never an unhandled 500). Bodies accumulate into one bounded buffer, so a many-chunk request stays close to the configured limit in memory. A JSON-RPC response object arriving on POST /mcp (this server never issues server-to-client requests) is rejected with 400 unexpected-json-rpc-response rather than a misleading -32600 with HTTP 200.

  • Filesystem names that Python decodes with surrogateescape (a POSIX filename with undecodable bytes, e.g. b"bad-\xff.txt" -> "bad-\udcff.txt") are never surfaced in JSON, because a lone surrogate cannot be encoded to strict UTF-8. Discovery (list_files, search) skips such entries so one undecodable filename does not break a listing; direct reads of such a path return the stable path-excluded error; and INCLUDE_LOCAL_PATHS=1 debug output omits any unsafe local path. The same text_safety helper backs the incoming-JSON lone-surrogate rejection.

  • Tool arguments are validated server-side against each tool's schema, and POST /call uses the same exact-type validation, including its envelope: tool must be a non-empty string and args must be an object when present — no str() coercion and no collapsing of falsy args. MCP tools/call.arguments is optional (omitted means {}); falsy values are rejected. Missing required properties, unknown properties, wrong types (a string is not a boolean, a boolean is not an integer), out-of-range integers (e.g. start_line above 1000000), over-length strings (paths capped at 1024 chars), and bad array items are refused with an invalid-params error. A tool that runs and fails (missing file, excluded path, unknown root) stays a normal MCP result with isError: true.

  • read_multiple_files treats max_chars as an aggregate caller-visible payload-string budget, not a serialized JSON or HTTP response cap. It counts the requested path, the generated reference, the error code, and the file content as Python Unicode characters, plus a fixed conservative per-item allowance for structure. Once exhausted, remaining caller-supplied paths are not emitted and a top-level truncated flag is set (using the budget exactly with nothing omitted is not truncation). The response reports limits.max_payload_chars / limits.used_payload_chars.

  • The MCP initialize handshake validates its parameters (protocolVersion non-empty string, capabilities object, clientInfo object with non-empty name/version) and rejects malformed requests with -32602. A supported requested protocol version is echoed back; an unsupported one is answered with the server's own selected version (2025-06-18) rather than claimed. This in-protocol negotiation is distinct from the HTTP MCP-Protocol-Version header check, which refuses the request outright with 400.

  • Requests over the limit return 429 with rate-limited and a Retry-After header. The limiter is keyed by socket peer address only and never trusts X-Forwarded-For or Forwarded. GET /health reports both configured_rate_limit_per_minute and effective_rate_limit_per_minute (in public mode an unset variable still applies the finite default).

Browser access control

Loopback binding is not a boundary on its own: a page you merely visit can aim a fetch() at 127.0.0.1. So every request must carry a Host that names this machine, and any Origin present must be loopback or explicitly allowed. Requests that fail return 403 with host-not-allowed or origin-not-allowed.

Variable

Default

Purpose

BRIDGE_ALLOWED_ORIGINS

(none)

Comma-separated exact origins, e.g. https://app.example.com

BRIDGE_ALLOWED_HOSTS

(none)

Comma-separated hostnames, e.g. bridge.example.com

curl and most MCP clients send no Origin, and those requests are accepted; token auth still applies. If you reach the bridge by any name other than localhost, 127.0.0.1, or ::1, add it to BRIDGE_ALLOWED_HOSTS.

BRIDGE_ALLOWED_ORIGINS is an anti-rebinding allowlist, not CORS. The bridge never emits Access-Control-Allow-Origin and does not handle OPTIONS preflight, so a browser-based cross-origin application cannot read responses from the bridge. That is deliberate: this is a file bridge for MCP clients and curl, not a public web API, and there is no wildcard CORS anywhere. To reach the bridge from a browser extension or same-origin web app, proxy it server-side from your own origin.

Excluded paths

The bridge skips common secret, runtime, generated-output, dependency, and cache paths during listing and search, and direct reads are refused for those paths. This includes:

  • .env and any .env.* variant, plus .envrc

  • token/secret/credential-prefixed filenames

  • SSH private keys (id_rsa, id_ed25519, ...) and the .ssh directory

  • credential stores: .netrc, .git-credentials, .npmrc, .pypirc, kubeconfig, service-account.json, and the .aws, .kube, .docker, .gnupg directories

  • key material and keystores by extension: .pem, .key, .crt, .p12, .pfx, .jks, .keystore, .kdbx, .p8, .der, .csr, .ppk, .asc, .gpg

  • databases and backups: .db, .sqlite, .sqlite3, .bak, .backup

  • runtime/settings.json, .git, virtualenvs, node_modules, common caches, and generated output folders

Two things worth knowing about how the matching works. The prefix rules are blunt on purpose: a file named tokenizer.py or tokens.css is hidden because it starts with token. Renaming it or scoping the root more narrowly is the fix; the bridge prefers hiding a harmless file over exposing a credential. And .log files are readable, since tailing a log is a normal reason to use this bridge.

This list is a safety net, not a guarantee. It cannot know what a secret looks like in your filesystem. Do not allowlist a directory if you would not be comfortable with an authorized client reading the ordinary text files inside it.

Exposing it beyond localhost

Don't, unless you mean it. If you put this behind a tunnel, set BRIDGE_PUBLIC_MODE=1 and a strong BRIDGE_TOKEN (>=32 chars). The bridge will refuse to start otherwise. Even then, only the dedicated read-only endpoints are remote-reachable; the generic /call dispatch stays localhost-only.

Status

Alpha. Read-only. Expect rough edges. Issues and PRs welcome.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    -
    quality
    D
    maintenance
    Enables file system operations such as listing, reading, and creating files within a scoped local project directory. It provides a secure way to manage local files through standardized MCP tools built with FastMCP.
  • F
    license
    -
    quality
    D
    maintenance
    Enables accessing and managing files from configured folders with filtering and size limits, allowing listing, reading, and searching files via MCP tools and resources.

View all related MCP servers

Related MCP Connectors

  • Search your AI chat history (ChatGPT, Claude, Codex) from any MCP client. Remote, private, read-only

  • Scans MCP servers for tool poisoning, prompt injection and supply chain risks.

  • Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wva2ccyk-prog/personal-mcp-bridge'

If you have feedback or need assistance with the MCP directory API, please join our Discord server