Skip to main content
Glama
soumosa

Notion Cowork Bridge

by soumosa

Notion Cowork Bridge

Give a Notion Custom Agent real file tools and a real terminal, over the Model Context Protocol. Runs on macOS, Linux, and Windows.

Before you spend ten minutes on this, check you can actually use it:

  • Notion Business or Enterprise, with Custom Agents and custom MCP connections enabled — a workspace owner may have to turn that on for you

  • Node 20 or newer, and an ngrok account (the free tier is enough)

  • A machine you are willing to give a remote agent a shell on. Read the warning below before you decide that.

I kept hitting the same wall: the plan, the spec, and the task list all lived in Notion, but the moment there was actual work to do I had to leave for a coding app, do the work there, and then come back and write down what happened. This bridge closes that loop. The agent reads a folder on my machine, edits files, runs Git, installs packages, runs the tests, and reports back — in the same thread where the task was written.

CAUTION

The terminal isnot sandboxed, and that's deliberate. A command can reach anything your user account can reach: files outside the workspace, your network, your developer credentials, and destructive system commands. The threat that actually catches people is prompt injection — text in a page or a file steering the agent into running something you never asked for. share_preview can put a locally running app behind a public URL that anyone holding the link can open, with no login. It safely shares the existing MCP hostname unless you configure a distinct preview endpoint. Please read Security and SECURITY.md before you install this.

What you get

Thirty-two tools. The character column comes from each tool's own MCP annotations, which is what Notion looks at when it decides whether to ask you first: "read-only" means readOnlyHint: true and "destructive" means destructiveHint: true. Where a tool needs more than an annotation to describe it — the terminal, the preview — I've said so instead.

Files — everything below is confined to the workspace.

MCP tool

What it does

Character

list_files

Lists folders and files, skipping node_modules and friends

Read-only

read_text_file

Reads UTF-8 files, whole or by line range

Read-only

read_bytes

Reads a byte range from any file, base64-encoded

Read-only

write_text_file

Creates or replaces a UTF-8 file

Destructive

edit_text_file

Replaces exact strings and returns a diff

Destructive

create_directory

Creates one folder

Write

delete_path

Deletes, into .bridge-trash unless you ask for permanent

Destructive

move_path

Moves or renames, never over an existing destination

Destructive

Search — same boundary, same ignore list.

MCP tool

What it does

Character

search_text

Regex search through workspace text files

Read-only

glob_files

Finds files by name pattern

Read-only

Terminal and background processes — not confined to anything.

MCP tool

What it does

Character

run_terminal_command

Runs your normal shell, with network access

Unrestricted

start_background_process

Spawns something meant to keep running and returns straight away

Unrestricted

list_background_processes

Shows what it started and whether it's still alive

Read-only

read_process_output

Reads a background process's log

Read-only

stop_background_process

Kills a background process and its whole group

Destructive

Preview and media

MCP tool

What it does

Character

http_probe

Sends one HTTP request to a loopback port and reads the answer

Loopback only

http_request

Sends a consequential POST/PUT/PATCH/DELETE request to loopback

Consequential

share_preview

Creates a temporary authenticated public endpoint for a local port

Public exposure

stop_preview

Revokes one preview link, or all of them

Destructive

read_media_file

Reads an image out of the workspace as an image

Read-only

capture_screenshot

Renders a loopback page to a PNG in the workspace

Write

Isolated browser — an ephemeral Chrome, Chromium, or Edge profile for checking a local app without exposing personal browser state.

MCP tool

What it does

Character

browser_status

Reports browser availability and sessions

Read-only

browser_open

Opens an allowed loopback app

Consequential

browser_snapshot

Lists page controls with opaque references

Read-only

browser_console / browser_network

Reads bounded diagnostics

Read-only

browser_screenshot

Returns a page PNG directly

Read-only

browser_interact

Clicks, types, selects, scrolls, or waits

Consequential

browser_eval

Runs bounded page-only JavaScript

Consequential

browser_upload

Uploads a workspace file through a file input

Consequential

browser_close

Closes the ephemeral session

Destructive

Policy

MCP tool

What it does

Character

workspace_info

Reports the active boundary, every enforced limit, live previews and background processes

Read-only

Twelve of those tools — the eight file tools, both search tools, and both media tools — are strict about where they can point: they reject absolute paths, reject .. traversal, and refuse to follow symlinks at any segment of the path. The terminal starts inside the workspace but is free to leave it, and share_preview deliberately hands a URL to the internet. That's the whole point of both of them, and also the reason the warning above is worded the way it is.

flowchart LR
    A["Notion Custom Agent"] -->|"HTTPS /mcp + bearer token"| B["Shared ngrok hostname"]
    H["Anyone holding a preview link"] -->|"one-time bootstrap URL"| B
    B --> C["Local bridge<br/>127.0.0.1:3210"]
    C --> D["Scoped file and search tools"]
    C --> E["Normal user shell"]
    C --> P["share_preview relay<br/>host-only cookie, expires"]
    B -->|"authenticated root paths"| P
    D --> F["Chosen workspace"]
    E --> F
    E --> G["Network and user-accessible files"]
    P --> L["A local dev server"]

Related MCP server: MCP Server

Where this fits next to Claude Cowork or Codex

If Notion is already where you plan, write specs, and track work, this gets you a surprising amount of the way there. You get a persistent agent with reusable instructions, your Notion pages and databases as project context, real local execution, scheduled and event-driven runs, and Notion's own activity history and sharing controls around all of it.

It is not a drop-in replacement for a dedicated coding agent. There's no code-review UI, no terminal emulation, no worktrees, no local checkpoints, and no editor-native diff. Commands are non-interactive, and a foreground command is capped at two minutes — anything meant to run longer has to go through the background-process tools. I use it for the kind of work that's mostly reading, small edits, and verification — not for a long refactor I'd want to watch closely.

What you need

  • macOS, Linux (systemd), or Windows 10/11

  • Node.js 20 or newer

  • an ngrok account with the agent authenticated

  • a Notion workspace where Custom Agents and custom MCP connections are enabled

  • optionally, Chrome, Chromium or Edge, if you want capture_screenshot to work. Nothing here bundles a browser or installs one, and every other tool works without it.

./scripts/bridge (or .\scripts\bridge.ps1 on Windows) is the one command to remember; it picks the right script for whatever you're on. Underneath, each platform still gets its own installer, service manager, and token store:

Installer

Runs under

Token stored in

macOS

install-macos.sh

launchd user agents

Keychain

Linux

install-linux.sh

systemd user units

file, mode 0600

Windows

install-windows.ps1

Scheduled Tasks at logon

DPAPI-encrypted file

Linux uses a 0600 file rather than a keyring because a desktop keyring is normally locked when a user service starts at boot — a keyring-backed token would fail to start about as often as it worked. SECURITY.md explains the trade.

Notion currently documents MCP connections for Custom Agents as a Business or Enterprise feature, and a workspace owner may additionally need to turn on custom servers under Settings → Notion AI → AI connectors → Enable Custom MCP servers. Notion's MCP integration guide and Custom Agents docs are the source of truth here, and both change fairly often.

Setup

1. Install the prerequisites

macOS:

brew install node ngrok

Linux — install Node 20+ from your distribution or nodesource, then ngrok from their Linux instructions:

sudo snap install ngrok    # or the .deb / .rpm / tarball

Windows:

winget install OpenJS.NodeJS.LTS
winget install ngrok.ngrok

Then, on every platform:

ngrok config add-authtoken YOUR_NGROK_AUTHTOKEN

Your ngrok dashboard assigns you a development domain that looks like example-name.ngrok-free.dev. You need that exact hostname for the next steps. The free plan gives you one assigned domain and background endpoints, within its usage limits.

2. Clone it and look around

git clone https://github.com/soumosa/notion-cowork-bridge.git
cd notion-cowork-bridge
npm ci
npm test

You can try the whole thing here, before any of the installer steps below, without registering a service or opening a tunnel:

npm run dev

That generates a throwaway token for this run only, starts the bridge on 127.0.0.1, and prints a ready-to-paste curl. Nothing is written outside the checkout, nothing survives Ctrl+C, and no ngrok is involved. If you just want to see what the thing is before deciding, start there.

3. Read the code

You're about to expose a shell to a remote agent, so read it before you go further. Up to 1.1.0 I could tell you to read one file, about 760 lines. That stopped being honest at twenty-one tools: a 2,500-line server.js is a file people say they've read. So it's now a server, a small library of primitives, and one file per group of tools.

File

Lines

What's in it

src/server.js

427

boot, express, auth, transport, shutdown, workspace_info

src/lib/config.js

144

every environment variable and every limit, in one place

src/lib/paths.js

169

the workspace boundary and the per-segment symlink guards

src/lib/audit.js

180

the audit log, its hash chain, and the OS-log mirror

src/lib/textfile.js

318

line scanning, atomic writes, the edit and diff engine

src/lib/errors.js

84

the stable error codes

src/lib/globs.js

79

the ignore list the three read tools share

src/tools/files.js

563

the eight file tools

src/tools/process.js

685

the terminal and the background processes

src/tools/preview.js

507

http_probe, share_preview, stop_preview

src/tools/media.js

270

images and screenshots

src/tools/search.js

195

search_text and glob_files

src/tools/index.js

48

the list of tool modules, and nothing else

3,669 lines across thirteen files, against 764 in one. That is a weaker claim than the one it replaces and I'm not going to dress it up as an upgrade. "Read this one file" is a promise a stranger can actually keep; "read these thirteen" is a promise most people will keep halfway.

What I can offer instead: no framework, no plugin system, no dependency injection, three runtime dependencies, and src/lib/README.md stating the contract a tool module signs — so reading one tool file tells you what that tool can reach without reading the rest. If you read only two, read src/lib/paths.js, which is the boundary, and src/tools/process.js, which is the thing that has no boundary.

4. Do a dry run first

There is one entry point for all of this. ./scripts/bridge works out which platform you're on and calls the right script underneath; on Windows it's .\scripts\bridge.ps1. The verbs are install, doctor, token, rotate, uninstall, logs, audit, and set-workspace. The per-platform scripts are all still there and still work if you'd rather call them directly.

Swap in your hostname and the folder you actually want to expose. macOS and Linux:

./scripts/bridge install \
  --host example-name.ngrok-free.dev \
  --workspace "$HOME/Desktop/notion-workspace" \
  --dry-run

Windows, from PowerShell:

.\scripts\bridge.ps1 install `
  -PublicHost example-name.ngrok-free.dev `
  -Workspace "$env:USERPROFILE\notion-workspace" `
  -DryRun

That validates your arguments and prints the plan without touching anything. If the plan looks right, run it again without --dry-run / -DryRun.

The installer copies a minimal runtime outside the repository, creates the workspace if it doesn't exist, generates a 64-character bearer token into the platform's token store, registers two per-user services (the bridge and the tunnel), starts them, and waits for the local health check to pass.

No sudo and no Administrator anywhere. The services come up at login and restart themselves if they die.

On Linux the installer also runs loginctl enable-linger, without which your user services stop the moment you log out. The parameter on Windows is -PublicHost, not -Host$Host is a reserved PowerShell variable.

If you want a second, independent check in front of the bearer token, pass --traffic-policy-file and point it at a copy of examples/ngrok-traffic-policy.yml. It makes ngrok answer 404 to anything that doesn't carry a header value you chose, so a scanner that finds your domain never learns there's an MCP server behind it. Optional, and SECURITY.md is honest about what it is and isn't.

5. Grab the token

./scripts/bridge token       # macOS and Linux
.\scripts\bridge.ps1 token   # Windows

Treat it like a password. Don't commit it, don't paste it into an issue, and don't park it in a Notion page — anyone with the token and the URL gets a shell on the machine you installed this on.

6. Wire up the connection in Notion

  1. Create or open a Notion Custom Agent.

  2. Open the agent's Settings.

  3. Under Tools & Access, choose Add connection.

  4. Choose Custom MCP server.

  5. Fill in:

    • URL: https://example-name.ngrok-free.dev/mcp

    • Name: Local Cowork Bridge

    • Authentication: Bearer token

    • Token: the value from step 5

  6. Connect, then save the agent.

  7. Leave the read-only tools on Run automaticallyworkspace_info, list_files, read_text_file, read_bytes, search_text, glob_files, read_media_file, list_background_processes, and read_process_output. None of them change anything.

  8. Leave everything else on Always ask: the writes, the deletes, run_terminal_command, start_background_process, capture_screenshot, and — most of all — share_preview, which is the one that hands a URL to the internet. Notion recommends this too, and I'd keep it that way well past the point where you've stopped being nervous about it.

7. Give the agent its operating instructions

Copy examples/AGENT_INSTRUCTIONS.md into the agent's Instructions field and adapt it to your project. Then start small:

Use Local Cowork Bridge. First report the workspace boundary and list the top-level files. Don't modify anything yet.

Once that works:

Inspect this project, explain how it's structured, and propose a short plan plus the checks that would disprove your plan.

And when you trust it:

Create a Bun project in experiments/hello, install its dependencies, run its tests, and report the exact commands and results.

How I actually use it

The loop that works for me:

  1. Link the Notion spec or task page so the agent has the requirement in front of it.

  2. Ask it to inspect the code without editing anything.

  3. Approve a bounded plan — and the check that would prove the plan wrong.

  4. Approve terminal and write calls one at a time. Read them first.

  5. Make it run the tests and then actually inspect the resulting files, not just the exit code.

  6. Review the diff myself before anything gets committed.

More prompts I reuse are in examples/PROMPTS.md.

Things worth knowing about the newer tools

Edits are edits now, not rewrites. edit_text_file takes a list of exact old string / new string pairs, requires each one to match exactly once unless you say otherwise, and returns a unified diff of what it changed instead of the whole file. If any edit in the list is missing or ambiguous, the entire call fails and nothing is written. dry_run: true shows you the diff without touching the file.

Both edit_text_file and write_text_file take an optional if_sha256. Pass the sha256 a previous write or edit returned, and the call is rejected with E_PRECONDITION_FAILED if the file has changed since. This is the thing that stops an agent from silently clobbering an edit you made in your editor while it was thinking. It hashes the file's exact bytes, so carry forward the sha256 from a prior write rather than hashing the text you read.

Searching no longer needs the shell. search_text runs a regex over workspace text files and glob_files matches names, both skipping .git, node_modules, dist, build, target, .venv, __pycache__, .bridge-trash and whatever the workspace's .gitignore excludes. That matters for more than convenience: every search the agent does this way is one fewer grep through run_terminal_command, which is one fewer approval prompt you have to read carefully.

Deleting has an undo. delete_path moves things into .bridge-trash/ rather than unlinking them, unless you pass permanent: true. Empty it yourself; nothing empties it for you.

Background processes. start_background_process spawns detached, redirects stdout and stderr to a log file, and returns immediately with an id. Use it for dev servers, watchers, anything that doesn't exit on its own. read_process_output tails the log, list_background_processes says what's alive, and stop_background_process kills the whole process group. Everything tracked is killed when the bridge shuts down.

http_probe sends one HTTP request to a port on 127.0.0.1 and gives back the status, timing, headers and body — with a text mode that strips scripts, styles and tags so an SSR page comes back readable. It takes a port number, not a URL, and it will not talk to anything off the loopback interface. That's deliberate: a hostname argument is something a poisoned README could point at your router.

share_preview puts a locally running app at the root of the existing public hostname, with a one-time bootstrap URL and a TTL, so you can actually open the thing the agent just built. /mcp and /health remain reserved for the bridge. It requires confirm_public: true, because it is a real exposure and I want you to have typed that. A distinct endpoint in MCP_NGROK_PREVIEW_URL remains an optional override. Read the section in SECURITY.md before you use it — a dev server behind a public URL gives away more than you think.

Screenshots. capture_screenshot renders a loopback page to a PNG in the workspace using the same isolated browser manager as the interactive browser tools, and read_media_file reads any image back as an image. No browser is bundled or installed. http_probe is cheaper and tells the agent more, so reach for the screenshot when the question is genuinely about how something looks.

Keeping it running

Check the whole installation at once with ./scripts/bridge doctor (.\scripts\bridge.ps1 doctor on Windows). It prints a PASS/FAIL line per check and exits non-zero if anything is wrong, and warns when the token is more than 90 days old. bridge logs follows the bridge's own process log; bridge audit follows the structured one.

The service commands underneath, if you want them directly:

macOS

tail -f "$HOME/Library/Logs/notion-cowork-bridge/bridge.log"
launchctl kickstart -k "gui/$(id -u)/com.notion-cowork-bridge.mcp"
launchctl bootout "gui/$(id -u)/com.notion-cowork-bridge.mcp"      # stop now
launchctl bootout "gui/$(id -u)/com.notion-cowork-bridge.tunnel"

Linux

journalctl --user -u notion-cowork-bridge -f
systemctl --user restart notion-cowork-bridge.service
systemctl --user stop notion-cowork-bridge.service \
  notion-cowork-bridge-tunnel.service                              # stop now

Windows

Get-ScheduledTask -TaskName NotionCoworkBridge | Get-ScheduledTaskInfo
Stop-ScheduledTask -TaskName NotionCoworkBridge
Start-ScheduledTask -TaskName NotionCoworkBridge
Stop-ScheduledTask -TaskName NotionCoworkBridgeTunnel               # stop now

Re-running the installer restores or updates both services.

The audit log

Every consequential action — terminal commands, background processes, writes, edits, deletes, moves, folder creation, previews shared, and failed authentication attempts — is appended as one JSON line. Each one is recorded before it happens and again after, so even a command that hangs and gets killed leaves a trace.

Platform

Location

macOS

~/Library/Logs/notion-cowork-bridge/audit.jsonl

Linux

~/.local/state/notion-cowork-bridge/audit.jsonl

Windows

%LOCALAPPDATA%\notion-cowork-bridge\audit.jsonl

It rotates at 10 MB and keeps five files. Every record also carries prevHash, the SHA-256 of the previous line, and is mirrored to the operating system's own log. What that buys is explained in SECURITY.md, including what it doesn't.

# What has the agent actually run?
grep '"run_terminal_command.start"' audit.jsonl | jq -r '.time + "  " + .command'

# Anything that failed or was killed?
jq -c 'select(.event == "run_terminal_command.finish" and (.exitCode != 0 or .timedOut))' audit.jsonl

# Anything that went near credentials, a keychain, or a pipe into a shell?
jq -c 'select(.flagged)' audit.jsonl

# Is somebody knocking who shouldn't be?
jq -c 'select(.event == "auth.failure")' audit.jsonl

Read it after any session where the agent touched content you didn't write yourself. The bearer token is never written to it.

It is a flight recorder, not a lock: the same process that runs the commands writes the log, and anything running as your user can edit it. The hash chain means you can tell afterwards that it happened.

Troubleshooting

Notion says it can't connect. Check the public endpoint yourself first:

curl -i https://example-name.ngrok-free.dev/health

A 200 with {"status":"ok"} means the tunnel and bridge are both fine and the problem is on the Notion side — usually the URL is missing the /mcp suffix.

Everything returns 401. The token in Notion doesn't match the stored one. Re-copy it with ./scripts/bridge token and paste it into the connection again. Watch for a trailing space; that bites more often than you'd expect. Repeated 401s from the same source get an escalating delay, up to five seconds, and each one is written to the audit log — so if jq 'select(.event == "auth.failure")' shows attempts you can't account for, that's somebody else.

The public health check fails but the local one passes. The tunnel is down. Look at tunnel.log — the usual causes are an expired ngrok authtoken, a different domain than the one you installed with, or another ngrok process already holding the domain. pkill ngrok and then kickstart the tunnel service.

bridge doctor reports a missing Keychain token on macOS. macOS prompts before letting a script read the Keychain, and a denied prompt looks identical to a missing entry. Run ./scripts/bridge token in Terminal and choose Always Allow.

The bridge won't start and bridge.log mentions MCP_AUTH_TOKEN. The launch service couldn't read the Keychain, so the server refused to boot with a short or empty token. Same fix as above.

A command times out at exactly two minutes. That's the hard cap on a foreground command and it isn't configurable from Notion. Split the work, run the long part yourself, or start it with start_background_process and poll read_process_output.

A second command fails saying one is already running. Only one foreground command runs at a time. The tool now waits up to ten seconds for the slot before giving up, so this only shows up when something genuinely long is in flight.

A command hangs and then fails with no useful output. Almost always it started something that outlived it. The tool waits for the child's output pipes to close, not just for the shell to exit, so npm run dev & holds the call open for the full timeout and then the whole process group is killed. Use start_background_process for anything meant to keep running, and read_process_output to see what it said.

Interactive prompts are a different and gentler failure: the child gets no terminal and its input is /dev/null, so sudo, an SSH passphrase, or an interactive git rebase fail immediately rather than hanging.

list_files says symbolic links are not allowed. That's working as intended: the file tools never follow a link, because a link is the easiest way out of the workspace. Use run_terminal_command if you genuinely need the target.

A terminal command failed unexpectedly. The bridge does not deny commands by pattern. Check the returned exit status and the audit record; commands that look credential-oriented or pipe a download into a shell are marked for review, not blocked.

A preview asset returns 404. Traversal is never forwarded, and Vite /@fs/ paths must resolve to real files inside the configured workspace. The same status also means a link expired or was stopped.

The machine went to sleep. Both services stop answering. It has to be logged in, awake, and online for any of this to work.

Rotating the token

Rotate whenever the token might have leaked, when someone leaves the agent's share list, or just periodically. bridge doctor warns once it's past 90 days.

./scripts/bridge rotate        # macOS and Linux
.\scripts\bridge.ps1 rotate    # Windows

It generates a fresh 64-character token, replaces the stored one, and restarts the bridge. The old token stops working immediately, so update the connection in Notion right after — ./scripts/bridge token prints the new value.

Changing the workspace

You no longer have to re-run the installer for this:

./scripts/bridge set-workspace /absolute/path/to/somewhere-else

It creates the folder if it isn't there, rewrites the config, and restarts the bridge. Your old workspace is left alone.

Updating

git pull --ff-only
npm ci
npm test
./scripts/bridge install \
  --host example-name.ngrok-free.dev \
  --workspace "$HOME/Desktop/notion-workspace"
./scripts/bridge doctor

The installer keeps your existing token, so the Notion connection survives an update untouched.

Uninstalling

Stop the services and remove their definitions:

./scripts/bridge uninstall        # macOS and Linux
.\scripts\bridge.ps1 uninstall    # Windows

Add --purge (or -Purge on Windows) to also delete the installed runtime, config, logs, audit log, and the stored token.

Your workspace is never deleted, purge or not.

Running it from source

Handy while you're changing the server. .env.example lists the same variables; keep the real token in your shell or a secret manager rather than in a file in the repo.

export MCP_AUTH_TOKEN="$(openssl rand -hex 32)"
export MCP_WORKSPACE_ROOT="$HOME/Desktop/notion-workspace"
export MCP_ALLOWED_HOSTS="localhost"
export MCP_PORT=3210
npm start
curl http://127.0.0.1:3210/health

Before you open a pull request:

npm run check
npm test
npm audit --omit=dev

The test suite needs a workspace outside the repo, since it verifies that the bridge refuses to modify its own files. It makes a temporary directory for itself; set TEST_WORKSPACE_ROOT if you want it somewhere specific. The Bun test skips itself if Bun isn't installed.

Configuration

Variable

Default

What it does

MCP_AUTH_TOKEN

none

Required bearer token, minimum 32 characters

MCP_WORKSPACE_ROOT

~/Desktop/notion-workspace

File-tool root, and where commands start

MCP_ALLOWED_HOSTS

none

Comma-separated public hostnames the server will answer for

MCP_PORT

3210

Loopback HTTP port

MCP_AUDIT_LOG

per-platform (above)

Where the JSON-lines audit log is written

MCP_SHELL

see below

Shell used for terminal commands

SHELL

Used as the shell on macOS and Linux when MCP_SHELL is unset

MCP_BROWSER_PATH

autodetected

Path to the Chrome, Chromium, or Edge executable used by all browser tools

MCP_NGROK_API_URL

http://127.0.0.1:4040

Local ngrok Agent API URL

MCP_NGROK_PREVIEW_URL

none

Optional distinct HTTPS endpoint instead of sharing the MCP hostname

NGROK_TRAFFIC_POLICY_FILE

none

Read by the installer as the default for --traffic-policy-file

The shell defaults to $SHELL if it's an absolute path, then /bin/zsh on macOS, /bin/bash on Linux, and powershell.exe on Windows. Commands are handed over as -lc for POSIX shells, -NoProfile -NonInteractive -Command for PowerShell, and /d /s /c for cmd.exe.

The server always binds to 127.0.0.1. The only public route is the one ngrok gives you.

Preview setup

By default, share_preview reuses the hostname already listed in MCP_ALLOWED_HOSTS. One active preview may own its root paths; /mcp and /health are never forwarded and the bootstrap token is exchanged for a host-only cookie before the app loads. This works with ngrok's single free development domain. If MCP_NGROK_PREVIEW_URL names a distinct reserved HTTPS origin, the bridge uses the separate-endpoint mode instead.

Because shared mode reserves /mcp and /health, an app whose public UI depends on either path needs a distinct endpoint. The optional edge-secret policy in examples/ngrok-traffic-policy.yml checks only /mcp; applying the secret to every path would intentionally block human preview traffic.

Security

The bearer token answers exactly one question: may this caller talk to the MCP server? It does nothing to constrain what an authenticated agent then does.

The threat most people underrate is prompt injection. Your agent reads Notion pages, comments, repository files and command output — any of which someone else may have written. Text in those places can steer the agent into running something you never asked for, and the approval prompt is your only defence. It wears out: after forty approved git status calls you will approve the forty-first without reading it. SECURITY.md goes into detail.

Assume run_terminal_command can:

  • read your SSH keys, cloud credentials, browser data, and anything else your user can read

  • modify or delete files anywhere outside the workspace

  • install packages and run their lifecycle scripts

  • send data over the network

  • call the Keychain, credential manager, or secret store available to your login

And assume share_preview puts whatever is listening on that port in front of anyone who ends up with the link.

How I'd deploy it:

  • use a dedicated user account or a disposable VM

  • expose a purpose-built workspace, never your home directory

  • keep the terminal, write, and preview tools on Always ask

  • never approve a command you don't understand

  • keep secrets out of the workspace

  • don't share an agent that has this connection with people you wouldn't hand a terminal to

  • stop both services when you're not using the bridge

The bridge does strip its own bearer token and the whole family of variables that point at it from child command environments, cap stdout and stderr against one combined ceiling, allow only one foreground command at a time, enforce a 120-second ceiling, audit-only flag risky-looking commands, audit failed authentication attempts, and write every consequential action to a hash-chained audit log. Those controls prevent accidents and let you reconstruct what happened. They are not a sandbox and won't stop a determined command.

The full threat model and the disclosure process are in SECURITY.md.

Known limits

  • The machine has to be logged in, awake, online, and running both services.

  • Linux needs systemd; there's no SysV or OpenRC installer.

  • On Windows the scripts are PowerShell 5.1+ and register Scheduled Tasks, so they need a real user session at logon.

  • Commands are non-interactive. Anything that prompts for input fails fast rather than hanging.

  • Command output is capped at 256 KiB across stdout and stderr together, keeping the first 64 KiB and the last 128 KiB.

  • Text reads are capped at 256 KiB and writes at 1 MiB. read_bytes gets you a range out of anything larger.

  • A foreground command tops out at two minutes. Background processes have no timeout and are killed when the bridge stops.

  • File, search, and media tools never follow symlinks.

  • capture_screenshot needs a browser you already have installed.

  • Shared-host mode supports one active preview and reserves /mcp and /health; configure a distinct endpoint only if an app needs those paths or you need two simultaneous previews.

  • Notion controls plan availability, credits, model access, and Custom Agent behavior, and any of it can change without notice.

  • ngrok's plan limits apply on top of everything else.

Contributing

Bug reports and pull requests are welcome — see CONTRIBUTING.md for what I look for. If you've found a security issue, please use GitHub's private vulnerability reporting instead of an issue.

Project status

This is an independent side project of mine. It isn't affiliated with or endorsed by Notion, Anthropic, OpenAI, or ngrok. "Notion," "Claude," "Cowork," "Codex," and "ngrok" belong to their respective owners.

License

MIT — Sourabh Morankar

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

Maintenance

Maintainers
Response time
0dRelease cycle
6Releases (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

View all related MCP servers

Related MCP Connectors

  • A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…

  • Connect AI agents to Replynodes over the Model Context Protocol.

  • Markdown-first MCP server for Notion API with 8 composite tools and 39 actions.

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/soumosa/notion-cowork-bridge'

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