scinote-mcp
Click on "Install 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., "@scinote-mcptick 'stain mixture combined' and note the color change"
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.
scinote-mcp
MCP (Model Context Protocol) server that exposes SciNote ELN bench workflows as tools, so an AI assistant (e.g. Ozwell hands-free chat) can drive them by voice: "tick 'stain mixture combined'", "consume 20 mils from A1 aliquot 2", "what's my next step?".
Architecture (see also docs/setup-gingaguard.md in scinote-web):
Ozwell HandsFreeChat (tablet) → chat backend (LLM) → scinote-mcp (this) → SciNote REST APIHard rule: every write goes through the SciNote REST API with the tech's own credentials. Never write to the SciNote database directly — API writes are what produce the audit trail, stock ledger, and permission checks. That property is the whole point of this server.
Setup
cd sidecar/scinote-mcp
npm install
cp .env.example .env # then fill in values
npm run dev # starts on stdio
npm run inspect # opens MCP Inspector UI to poke at toolsAuthentication
Two options (see TokenAuthentication concern in scinote-web):
Api-Key header (recommended to start): requires
CORE_API_KEY_ENABLED=trueon the SciNote server; each user has an API key (users.api_key). SetSCINOTE_API_KEY.JWT Bearer: a token whose
issmatches the server'score_api_token_iss. SetSCINOTE_JWT.
Both are gated by server-side flags. On the dev instance these are set on the
scinote-web systemd user unit:
Environment=CORE_API_V1_ENABLED=true
Environment=CORE_API_KEY_ENABLED=trueWithout CORE_API_V1_ENABLED, /api/v1/* returns 404 while /api/status
still answers 200 — that combination means the routes were never mounted.
Mint a key for a user with:
bundle exec rails runner "puts User.find_by(email: 'admin@scinote.net').regenerate_api_key!"Trying it out
npm run smoke drives the server the same way an MCP client does. Several
tool/args pairs run against one server process, which is how you set the
working scope before a scoped call:
npm run smoke # list registered tools
npm run smoke -- scinote_status list_teams
npm run smoke -- set_scope '{"team":"Dalaly","project":"Polymicrobial","experiment":"GingiGuard Assay"}' list_tasks
npm run smoke -- set_scope '{...}' get_task_steps '{"taskId":"970"}'Picking what to work on
There's no team/project/experiment in .env. The tech chooses at runtime —
"work on the GingiGuard assay" — and set_scope resolves the name and
remembers it, so nothing downstream has to say ids out loud.
That selection is keyed by the caller's credential rather than a connection, because HTTP requests are stateless here: each one builds a fresh server. Two techs on different runs therefore keep separate scopes, and a request that needs a scope it doesn't have answers with the tools to call instead of a 404.
scinote_status what's selected right now
list_teams / list_projects / list_experiments / list_inventories
set_scope { team?, project?, experiment? } by name or idThe ids are nested, so choosing a team clears the project under it and choosing a project clears the experiment.
Sanity check your credentials:
curl -s -H "Api-Key: $SCINOTE_API_KEY" $SCINOTE_BASE_URL/api/v1/teams | head -c 400Running it
Two transports share one set of tool definitions (src/server.ts):
Command | Transport | Credential |
| stdio, for MCP Inspector and editor clients that spawn a child process | the one in |
| Streamable HTTP on port 3001, for remote clients like Ozwell on a phone | per request, from the caller's headers |
An MCP server binds to exactly one transport, so the HTTP listener builds a
fresh server per request via createServer(). That also keeps concurrent techs
from ever sharing a session.
The HTTP endpoint carries no ambient authority
A publicly reachable endpoint holding a shared admin key would hand anyone on the internet full write access to the ELN. So every HTTP request must present its own SciNote credential:
Authorization: Bearer <the tech's own API key, or a JWT>
or
X-SciNote-Api-Key: <the tech's own key>A bearer token shaped like a JWT (header.payload.signature) is forwarded as
one; anything else is forwarded as an API key. SciNote's JWT decoder crashes on
non-JWT input, so the shape has to decide before the token leaves here.
Requests without one get a 401. The credential travels to src/scinote.ts via
an AsyncLocalStorage context, never a module global, so requests can't leak
credentials into each other. SciNote stays the authorization boundary — the
sidecar can't do anything the signed-in tech couldn't do in the browser, and
the audit trail names a real person.
SCINOTE_MCP_ALLOW_SHARED_CREDENTIAL=true falls back to the .env credential
for local testing. Never set it on a reachable deployment.
Behind nginx
The dev deployment is proxied at https://scinote-mcp.os.mieweb.org/ → port
3001. List every hostname the proxy forwards in SCINOTE_MCP_ALLOWED_HOSTS;
anything else is rejected as a DNS rebinding attempt. Set
SCINOTE_MCP_BIND=0.0.0.0 if nginx runs on a different host than this one
(otherwise leave it on loopback).
location / {
proxy_pass http://10.17.74.237:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "";
proxy_buffering off; # required — MCP can stream responses
proxy_cache off;
proxy_read_timeout 3600s;
}Smoke test the deployed endpoint:
curl -s https://scinote-mcp.os.mieweb.org/healthz
curl -s -X POST https://scinote-mcp.os.mieweb.org/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $SCINOTE_API_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Instructions travel with the server
A bench tech shouldn't have to paste a prompt to get a usable assistant, and a
prompt pasted per client drifts out of step with the tools. So the guidance
lives in src/instructions.ts and ships over the wire in two forms:
Where it goes | Use it for | |
|
| Always-on facts about this server — the scope model, the |
| the | The hands-free loop, invoked by name when a run starts |
The split is deliberate. Instructions are unconditional, so they stay short and factual. The read-one-step-and-wait loop is a workflow choice that shouldn't be forced on someone using these tools for something else, so it's a prompt the tech opts into.
curl -s -X POST https://scinote-mcp.os.mieweb.org/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H "Authorization: Bearer $SCINOTE_API_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"prompts/list"}'Client support for instructions varies, so anything the run genuinely depends
on belongs in a tool description too — those are always read.
Logs
The dev deployment runs as a systemd user unit,
~/.config/systemd/user/scinote-mcp.service, alongside scinote-web and
scinote-worker. It restarts on failure and comes back after a reboot
(loginctl enable-linger is already on for this user).
systemctl --user restart scinote-mcp # after a code change; tsx doesn't reload
journalctl --user -u scinote-mcp -fOne line per request — status, which kind of credential arrived (never its value), and the tool that was called:
2026-08-30T04:21:58.163Z POST /mcp 200 auth=bearer tools/call list_teams
2026-08-30T04:21:58.197Z POST /mcp 401 auth=noneauth=none on a 401 means no credential header arrived at all — check that the
client is actually sending the one you configured.
Useful references
Route map:
scinote-web/config/routes.rb(searchnamespace :v1)Controllers (payload shapes + permitted params):
scinote-web/app/controllers/api/v1/*.rbSerializers (response shapes):
scinote-web/app/serializers/api/v1/*.rbTest data on the dev instance: team 14 / project 16 / experiment 166, task "Arm A1 - Run 1 (TEST)", inventory "GingiGuard Assay Reagents" (repository 2)
The API speaks JSON:API: collections are { data: [{ id, type, attributes, relationships }] }; writes send the same envelope.
Related MCP server: NGS360 MCP Server
Milestones
M0–M4 are done — all ten tools are implemented and verified against the dev instance. M5 is the remaining work. The notes below record what each milestone required and how it was accepted.
M0 — Environment (half a day) — done
npm install && npm run typecheckpass.envfilled in;curlauth sanity check returns JSON, not 401npm run inspectopens andscinote_statusreturns versions
Accept: screenshot of MCP Inspector showing a successful scinote_status call.
M1 — Read the world (1 day) — done
list_tasksreturns the test task(s) of experiment 166get_task_stepsreturns the 21 checkpoints withcompletedflagsExtend
get_task_stepsto also surface checklists and their items (ids +checked) — theinclude=checklists,checklists.checklist_itemsdata arrives in the JSON:APIincludedarray
Accept: get_task_steps output shows step P3.1 with its Actions checklist items and ids.
M2 — Execute a protocol (2 days) — done
Implement
tick_checklist_itemandcomplete_stepReturn human confirmations ("Ticked 'Blower on, 5 min' — 2 of 3 actions done on P1.1"), not raw JSON
Error mapping: 403 → "you don't have permission", 404 → "that step doesn't exist", stale id → suggest re-running
get_task_steps
Accept: from MCP Inspector, tick all actions on a step and complete it; the change is visible in the SciNote web UI and in the task's Activities feed attributed to your user.
M3 — Inventory + stock (2 days) — done
Implement
list_task_items,assign_item,consume_stockconsume_stockechoes item name, amount, and resulting stock in its confirmation; the tool description instructs the LLM to confirm with the user first — keep it that wayVerify in SciNote: stock decrements, ledger row appears (item card → stock export), low-stock warning shows when you cross the threshold
Gotcha: the API's
stock_consumptionis the cumulative total for that task assignment; the ledger derives the delta. A tech saying "log another 20 mL" means a delta, soconsume_stockreads the current total and sends the sum. Sending the raw amount would silently rewrite history.
Accept: full P3.1 flow via Inspector — assign "A1 - Aliquot 2", consume 20 mL, item shows 30 mL in SciNote with a ledger entry.
M4 — Search + results (1–2 days) — done
find_inventory_item: name match over inventory items ("a1 aliquot two" → row 63), reports stock. Voice input is sloppy, so the query and the item name are both normalized (lowercased, punctuation dropped, number words mapped to digits) before matchingadd_result_note: create a text result on the task (results controller, v1; v2 has richer result elements if needed)
Accept: "find A1 aliquot 2" round-trips to the right row id; a result note appears on the task.
M5 — Ozwell wiring (separate app, 3–5 days)
Not in this repo. Stand up a small chat backend that:
Mounts
HeyOzwell/HandsFreeChatfrom @mieweb/ui (start withreviewBeforeSend: true,transcription: "browser")Connects an LLM to this MCP server (any MCP-capable client/orchestrator)
Carries the logged-in tech's SciNote credential per session — the HTTP transport reads it from
Authorization: Bearer/X-SciNote-Api-Keyon every request, so the client just has to forward it (see "The HTTP endpoint carries no ambient authority")System prompt — the server ships its own, so no client has to be configured with one (see "Instructions travel with the server")
Accept: on a tablet, say "hey ozwell — disk stained and rinsed, done" and watch the step complete in SciNote.
Later / hardening
Task-scoped sessions ("I'm working Arm A1 Run 2" pins taskId so the tech never says ids)- Idempotency: repeating "consume 20 mL" must not double-log — read
stock_consumptionfirst and set, don't addRate limiting, retries with backoff, structured logging
Testing tips
MCP Inspector (
npm run inspect) is your main harness — no LLM neededWatch the Rails log on the dev box (
tail -f ~/scinote-web/log/development.log) to see your requests hit controllersEvery write should be visible in three places: the SciNote UI, the task Activities feed, and (for stock) the item ledger. If any of the three is missing, something is wrong.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Zero-setup MCP gateway securely connecting AI to your tools with authentication and workflows
AI-powered bioprotocol optimization — generate, search, and manage lab protocols via MCP
MCP connector that lets ChatGPT list, search, and run your Apple Shortcuts via a local Mac agent
Automate 1,000+ services from any MCP-compatible AI agent: build Applets, run actions and queries.
Related MCP Servers
- -licenseNot gradedqualityNot gradedmaintenanceProvides seamless integration between MCP-compatible AI assistants and n8n workflow automation, enabling intelligent management and automation of n8n workflows through natural language.5

NGS360 MCP Serverofficial
FlicenseCqualityBmaintenanceExposes the NGS360 bioinformatics platform REST API as MCP tools, enabling AI assistants to manage sequencing runs, projects, workflows, and more through natural language.82- AlicenseNot gradedqualityDmaintenanceExposes the Adaptyv Foundry protein characterization API as MCP tools, enabling AI assistants to interact with experiments, targets, sequences, and results in natural language.MIT
- FlicenseNot gradedqualityCmaintenanceExposes task management (add, list, complete tasks) and document search (RAG) as MCP tools for AI agents.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/BioNanomics/scinote-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server