NM

nikowru/mcp

Developer tools
0 stars 0 forks Quality 61 Trend 20

A custom MCP (Model Context Protocol) file-server connector that gives several Claude Code sessions — , , — one shared, sandboxed note-taking workspace, each role scoped to a different set of tools...

Overview

A custom MCP (Model Context Protocol) file-server connector that gives several Claude Code sessions — , , — one shared, sandboxed note-taking workspace, each role scoped to a different set of tools by its own bearer token. Built for a CS Midterms Pt. 1 project. - REPORT.md — the written summary: design, threat list, what the two demo runs actually showed. - DEMO_SCRIPT.md — 5-minute live demo plan with a network-failure fallback. - docs/design-notes.md — every design decision and why. - docs/security-notes.md — what was attacked and what held. - docs/transcripts/run1/, run2/ — real session transcripts from both demo runs. This README gets the whole thing running from a fresh clone on Windows. workspace/ is git-ignored because it's generated state, so the demo results are committed separately under docs/demo-output/ — see that folder's own README for how the three views (transcripts, logs, final files) relate.

README

notes-hub

A custom MCP (Model Context Protocol) file-server connector that gives several Claude Code sessions — builder, writer, auditor — one shared, sandboxed note-taking workspace, each role scoped to a different set of tools by its own bearer token. Built for a CS Midterms Pt. 1 project.

  • REPORT.md — the written summary: design, threat list, what the two demo runs actually showed.
  • DEMO_SCRIPT.md — 5-minute live demo plan with a network-failure fallback.
  • docs/design-notes.md — every design decision and why.
  • docs/security-notes.md — what was attacked and what held.
  • docs/transcripts/run1/, run2/ — real session transcripts from both demo runs.

This README gets the whole thing running from a fresh clone on Windows.

Submission map

Deliverable Where it is
Repository + README this repo, this file
Transcript of a multi-step task docs/transcripts/run2/ (and run1/) — three sessions per run, builder → writer → auditor
PLAN_LOG / CHECKPOINTS evidence docs/demo-output/run2/PLAN_LOG.md, CHECKPOINTS.md (plus PROJECT_STATE.md, DECISIONS.md, AGENT_GUIDE.md); run1/ for comparison
Tool-call logs (efficiency evidence) docs/demo-output/logs/tool_calls_run1.jsonl, tool_calls_run2.jsonl
Report: stack, tool design, security tradeoffs REPORT.md
Report appendix: claim-by-claim verification, full limitations, bug and threat detail docs/report-appendix.md
Screenshots: role tool lists, hardening, token-length check, tests, health checks docs/screenshots/ (embedded in REPORT.md and the appendix)
5-minute live demo plan DEMO_SCRIPT.md
Design decisions, with reasoning docs/design-notes.md
Attack attempts and results docs/security-notes.md
Day-to-day usage after the course docs/USING_NOTES_HUB.md

workspace/ is git-ignored because it’s generated state, so the demo results are committed separately under docs/demo-output/ — see that folder’s own README for how the three views (transcripts, logs, final files) relate.

Quick verify in 10 minutes

For a marker or anyone who just wants to confirm this works. Needs only Python and git — no ngrok account, no pm2, no Claude Code, and none of my credentials. Every command below was run from a fresh clone of this repo before being written here.

# 1. Clone, create the venv, install (~2 min)
git clone https://github.com/nikowru/mcp.git notes-hub
cd notes-hub
py -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt

# 2. Run the test suite - no .env needed, the tests supply their own tokens
.venv\Scripts\python.exe -m pytest
#    expect: 77 passed   (on macOS/Linux: 74 passed, 3 skipped - see below)

# 3. Make a throwaway .env with three random tokens
copy .env.example .env
.venv\Scripts\python.exe -c "import secrets; print(secrets.token_urlsafe(32))"
#    run that three times; paste one value into each of NOTES_TOKEN_BUILDER,
#    NOTES_TOKEN_WRITER and NOTES_TOKEN_AUDITOR in .env. Leave the ngrok vars empty.

# 4. Start the server (leave this terminal running)
.venv\Scripts\python.exe -m server.app
#    expect: notes-hub: listening on http://127.0.0.1:8000/mcp (bearer token required)

Then, in a second terminal, confirm auth works. 401 is the healthy response to an unauthenticated request:

cd notes-hub
$init = '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"check","version":"1"}}}'

# no token -> 401
curl.exe -s -o NUL -w "%{http_code}`n" -X POST http://127.0.0.1:8000/mcp `
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d $init

# wrong token -> 401
curl.exe -s -o NUL -w "%{http_code}`n" -X POST http://127.0.0.1:8000/mcp `
  -H "Authorization: Bearer not-a-real-token" `
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d $init

# a real token from your .env -> 200
curl.exe -s -o NUL -w "%{http_code}`n" -X POST http://127.0.0.1:8000/mcp `
  -H "Authorization: Bearer " `
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d $init

Finally, see the role scoping — the point of the project — with MCP Inspector (needs Node; npx downloads it on demand). Run it once per token:

npx -y @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --transport http `
  --header "Authorization: Bearer " --method tools/list
Token Tools returned delete_file
NOTES_TOKEN_BUILDER 10 present
NOTES_TOKEN_WRITER 9 absent
NOTES_TOKEN_AUDITOR 5 (read-only) absent

Same server, same URL, three different tool lists — the token alone decides. That is the whole claim, verifiable in about ten minutes without any account.

Seeing the demo without running it: the sessions’ actual output is committed, so nothing needs to be executed — see Where the evidence lives.

Where the evidence lives

The demo was run twice and its results are in the repo. None of it requires running anything:

Evidence Path
Session transcripts, both runs docs/transcripts/run1/, docs/transcripts/run2/ (3 per run)
PLAN_LOG.md, CHECKPOINTS.md, PROJECT_STATE.md, DECISIONS.md, AGENT_GUIDE.md as each run left them docs/demo-output/run1/, docs/demo-output/run2/
Workspace file tree and the workspace’s own git log docs/demo-output/run*/workspace-tree.txt, workspace-git-log.txt
Tool-call audit logs, sanitized docs/demo-output/logs/tool_calls_run1.jsonl, tool_calls_run2.jsonl
Screenshots docs/screenshots/ (embedded in REPORT.md)
What each file is and how they relate docs/demo-output/README.md

workspace/ itself is git-ignored — it’s generated state — which is why the results are committed separately under docs/demo-output/.

Built and tested on

Version
OS Windows 11 Pro, build 10.0.22631
Python 3.13.14 (via the py launcher)
Node.js v24.19.0 (npm 11.17.0)
pm2 7.0.4
ngrok 3.39.11
git 2.53.0.windows.2
Claude Code 2.1.301

The server code itself is platform-neutral Python, and the test suite is written to pass on macOS and Linux too: of 77 tests, 74 run everywhere and 3 skip off Windows (a real junction via mklink, a backslash path separator, and a UNC path — all cases where the OS’s own path semantics are the thing under test). The reserved-device-name and alternate-data-stream checks are pure string logic and run on every platform. That skip behaviour is derived from the path-handling code, not verified on a real macOS/Linux machine — see “What was and wasn’t verified” at the end.

The surrounding setup (pm2 process names, the Task Scheduler logon task, the py launcher) is Windows-specific by design.

Prerequisites

  • Windows 10/11
  • Python 3.13+ via the py launcher (the Windows Store python alias is broken on the dev machine, so every command below uses py or the venv interpreter directly)
  • git

Only needed to expose the server beyond localhost (steps 7–9, all optional):

  • Node.js — for pm2, and for MCP Inspector via npx
  • ngrok — a free account includes one static “dev domain”

If you only want to confirm it works, use Quick verify in 10 minutes — that path needs none of the optional prerequisites above, and no Claude Code either.

The numbered steps below are the full rebuild. Steps 1–6 give you a working server on localhost. Steps 7–9 add the public tunnel and process management. Steps 10–11 drive it from real Claude Code sessions, which is how the recorded demo was run; each session’s .mcp.json falls back to http://127.0.0.1:8000/mcp when NOTES_MCP_URL isn’t set, so those steps work against localhost too.

The live endpoint in this repo is not available to you. The ngrok domain referenced in .env.example and the recorded transcripts points at the author’s laptop, behind the author’s ngrok account, and only answers while that machine is on and logged in. It is deliberately not a hosted service. Nothing in this README depends on reaching it: every step runs against your own localhost, and the recorded demo results are committed under docs/demo-output/ and docs/transcripts/.

1. Clone and create the virtual environment

git clone 
cd Workspace
py -m venv .venv
.venv\Scripts\python.exe -m pip install -r requirements.txt

Everything below that runs Python uses .venv\Scripts\python.exe explicitly. This matters most for the tests: plain py -m pytest fails with No module named pytest, because pytest is installed only inside .venv/.

2. Configure .env

copy .env.example .env

Fill in:

  • WORKSPACE_ROOT, NOTES_HOST, NOTES_PORT — the defaults (./workspace, 127.0.0.1, 8000) are fine.
  • NOTES_TOKEN_BUILDER, NOTES_TOKEN_WRITER, NOTES_TOKEN_AUDITOR — one random token per role, generated with:
    .venv\Scripts\python.exe -c "import secrets; print(secrets.token_urlsafe(32))"
    
    Run it three times, one value per variable. Never commit .env or paste these tokens anywhere else — .env is already git-ignored, and the three tokens are the only thing separating the roles.
  • NGROK_AUTHTOKEN and NGROK_DOMAIN — only needed for steps 7–9. Leave both empty for a localhost-only rebuild. NGROK_DOMAIN is the hostname only, no https:// prefix; free accounts get one automatically under https://dashboard.ngrok.com/domains. Note that ecosystem.config.js refuses to start if NGROK_DOMAIN is empty, since a tunnel with no domain is useless — that’s expected if you’re skipping the tunnel.
  • NOTES_LOG_FILE, NGROK_BIN, NOTES_MCP_URL — all optional, documented inline in .env.example.

3. Generate the per-machine session permission rules

.venv\Scripts\python.exe scripts\setup_sessions.py

Writes sessions/{builder,writer,auditor}/.claude/settings.local.json, each holding Read deny rules naming this machine’s absolute path to .env and logs/. Those sit two directories above each session folder, and Claude Code’s permission syntax has no ../ anchor — only // (filesystem root), ~/ (home), / (settings-file directory) and ./ (current directory) — so the path must be generated per machine rather than committed. The file is git-ignored; re-run this script whenever the repo moves to a different path or machine.

4. Seed the demo notes

.venv\Scripts\python.exe scripts\seed_demo_notes.py

Writes 12 deliberately messy fake study notes into workspace/inbox/ — inconsistent naming and extensions, two near-duplicate pairs, one broken internal link, and one note containing a real (harmless, clearly labelled) prompt-injection payload, so the demo has genuine problems to find. Written directly to disk, not through notes-hub’s tools, since this is one-time setup. Safe to re-run; it only touches those 12 filenames.

5. Run the test suite

.venv\Scripts\python.exe -m pytest

77 tests should pass: sandbox escape attempts (traversal, UNC paths, drive letters, NTFS alternate data streams, Windows reserved device names, a real junction), role scoping at both list time and call time, versioned writes, str_replace match counting, delete-to-trash, the bad-write guards, logging redaction, and the handoff-file bootstrap.

6. Run the server locally (sanity check)

.venv\Scripts\python.exe -m server.app

Expect:

notes-hub: workspace root = \workspace
notes-hub: listening on http://127.0.0.1:8000/mcp (bearer token required)

Optionally confirm with MCP Inspector from another terminal — this should list all 10 tools for the builder token, and only 5 for the auditor token:

npx -y @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --transport http --header "Authorization: Bearer " --method tools/list

Stop it with Ctrl+C once confirmed; step 8 runs it under pm2 instead.

7. Expose it over ngrok with a static domain

ngrok config add-authtoken 
ngrok http --url=https://.ngrok-free.dev 8000

A 401 from the public URL is the healthy response — the tunnel and server are up, and auth is refusing an unauthenticated request. The domain is read out of .env rather than typed, so it isn’t echoed:

The static domain means the URL survives restarts, so each session’s .mcp.json only has to be written once. The server validates the Host/Origin headers against an allowlist built from NGROK_DOMAIN (DNS-rebinding protection) — if you get 421 Misdirected Request, NGROK_DOMAIN in .env doesn’t match the domain you’re actually tunnelling.

8. Run both processes under pm2

npm install -g pm2

Edit ecosystem.config.js first: it hardcodes absolute paths to .venv\Scripts\python.exe, the project root, and the local ngrok.exe, plus the --url domain. Update all of them for your machine.

pm2 start ecosystem.config.js
pm2 save

pm2 save records the process list so pm2 resurrect can restore it. Both processes online:

9. Reboot persistence (Task Scheduler)

pm2 startup isn’t supported on Windows — it only generates systemd/launchd unit files. Instead, a Task Scheduler task runs pm2 resurrect at logon:

$action = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$PWD\scripts\pm2-resurrect.ps1`""
$trigger = New-ScheduledTaskTrigger -AtLogOn -User "$env:COMPUTERNAME\$env:USERNAME"
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
    -StartWhenAvailable -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName "notes-hub-pm2-resurrect" `
    -Action $action -Trigger $trigger -Settings $settings `
    -Description "Restarts notes-hub-server and notes-hub-ngrok via pm2 resurrect after login (pm2 startup isn't supported on Windows)."

These flags were read back off the live task with Export-ScheduledTask, not written from memory, so this reproduces the task as it actually exists. The original one-off invocation was typed by hand and never saved, so treat this as a faithful reconstruction of the resulting task rather than a transcript of the keystrokes.

Verify:

Get-ScheduledTask -TaskName "notes-hub-pm2-resurrect" | Format-List TaskName, State
Start-ScheduledTask -TaskName "notes-hub-pm2-resurrect"
(Get-ScheduledTaskInfo -TaskName "notes-hub-pm2-resurrect").LastTaskResult   # 0 = success

Export-ScheduledTask -TaskName "notes-hub-pm2-resurrect" prints the authoritative definition on your own machine. It isn’t committed here because it embeds a Windows account SID and MACHINE\username.

This only restores the processes after an interactive logon — nothing runs before a user logs in, which would need both processes wrapped as real Windows services.

10. Connect each role’s Claude Code session

Each of sessions/{builder,writer,auditor}/ is its own Claude Code working directory, with its own .mcp.json (same URL, different Authorization: Bearer ${NOTES_TOKEN_} header) and its own .claude/settings.json (auto memory off; Write, Edit and Bash denied; reads confined to that folder).

For each role, in a separate terminal:

$env:NOTES_TOKEN_BUILDER = ""   # or WRITER / AUDITOR
# Only if connecting through the tunnel instead of localhost:
$env:NOTES_MCP_URL = "https://.ngrok-free.dev/mcp"
cd sessions\builder                                                # or writer / auditor
claude

Set only that role’s token variable in that terminal, and do not source the whole .env into it — .env holds all three tokens, and a shell that has them all defeats the isolation this setup exists to demonstrate. Each folder’s SESSION_SETUP.md explains why.

NOTES_MCP_URL is read by Claude Code (not the server) when it expands ${NOTES_MCP_URL:-http://127.0.0.1:8000/mcp} in that session’s .mcp.json. Leave it unset to talk to the local server. Inside a session, /mcp shows whether notes-hub connected and which tools your role got.

11. Run the demo

docs/demo_prompts.md has the exact first message for each session. Run them in order — builder, then writer, then auditor — since each depends on what the previous one left behind. DEMO_SCRIPT.md is the timed 5-minute presentation version.

12. Reset between runs

.\scripts\reset_demo.ps1          # prints its plan, then asks before touching anything
.\scripts\reset_demo.ps1 -Force   # same, no confirmation prompt

Archives the finished run rather than deleting it: workspace/ becomes workspace_runN/, logs/tool_calls.jsonl becomes logs/tool_calls_runN.jsonl, and docs/transcripts/*.txt move into docs/transcripts/runN/. The run number auto-increments, so a second reset can’t overwrite the first archive. It then restores workspace_before_demo/ (read-only; never modified), clears the five handoff files so the server regenerates them from the current templates, and restarts pm2.

The only deletions are those handoff files, and only after scripts/check_handoff_pristine.py confirms by hash that each is still an unmodified template — anything a session actually wrote is kept and reported instead. The script finishes by printing pm2 status, the restored note count, whether all five handoff files came back, and whether the regenerated AGENT_GUIDE.md contains the PROJECT_STATE step.

Requires workspace_before_demo/ to exist — a pristine copy of the workspace taken before the first run. It’s git-ignored (local backup, not source), so a fresh clone won’t have one; re-run scripts/seed_demo_notes.py instead, or copy a clean workspace/ aside before the first demo run.

What was and wasn’t verified

These instructions were dry-run from a genuine fresh git clone into a temporary directory outside the project, following only this README, with throwaway tokens in a separate .env. Verified working end to end:

  • clone → py -m venv → pip install -r requirements.txt (all six pins resolved)
  • scripts/setup_sessions.py — generated correct absolute deny rules for the new clone location, confirming it isn’t tied to one path
  • scripts/seed_demo_notes.py — 12 notes written
  • .venv\Scripts\python.exe -m pytest — 77 passed in the clone
  • python -m server.app booted, and over real HTTP: no token → 401, wrong token → 401, valid builder token → 200 with a well-formed MCP initialize result

Not dry-run, and why:

  • ngrok, pm2, the Task Scheduler task — these bind to one machine’s account, ports and reserved domain. Re-running them would have disturbed the live setup the demo evidence came from. The Task Scheduler command in step 9 was instead read back off the registered task with Export-ScheduledTask.
  • A real Claude Code session against the clone — would need a fourth concurrent session and its own tokens; role scoping is covered over real HTTP by tests/test_roles.py, which passed in the clone.
  • macOS/Linux — no such machine was available (wsl.exe is present but has no distro installed, and installing one wasn’t in scope). The three platform skips are reasoned from the path-handling code and verified only by forcing the skip condition, not by a real run. Treat “passes on macOS/Linux” as expected-but-unproven.
  • MCP Inspector (the optional check in step 6) — not run in the clone; it needs an npx download.
View this README on GitHub

Install

npx -y @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --transport http