NitroWatch
Click on "Deploy 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., "@NitroWatch what pending actions need my approval?"
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.
NitroWatch
Your AI agent just got a permission system.
Connect an AI agent to an MCP server today and it is all-or-nothing: either the agent can call every tool, or a human hand-approves every single call, forever. There is no policy layer, no notion of which tools are dangerous, no audit trail, and no way for trust to grow over time.
NitroWatch is the missing governance layer. It sits in front of any MCP server, classifies every tool by risk, enforces what the agent may do, lets safe tools earn autonomy through a proven track record, and records every decision permanently.
Built with the NitroStack TypeScript SDK for the NitroStack Γ SRM Hackathon 2026.
π‘οΈ Read the full walkthrough β
The problem MCP has today, the three risk tiers, how autonomy is earned, and the six bugs we found while building this (two of them in NitroStack's own framework, reported upstream).
The problem
Imagine a company's MCP server exposing three tools:
Tool | Consequence |
| Harmless. Reads data. |
| Costs money, but can be voided. |
| Irreversible. Gone is gone. |
MCP treats all three identically. There is no way to express "let the agent read freely, ask me before it spends money, and never let it delete anything."
Every team adopting MCP hits this wall immediately, and both usual answers are bad:
Approve everything manually β the human becomes the bottleneck and starts rubber-stamping.
Trust the agent fully β one hallucinated tool call destroys production data.
NitroWatch is the middle path nobody has built.
Related MCP server: Oakallow MCP Server
How it works
AI AGENT
β request_action(serverId, toolName, args)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββ
β NITROWATCH β
β β
β 1. classify every tool β a risk tier β
β 2. enforce tier decides what happens β
β 3. earn clean record β autonomy β
β 4. audit every decision recorded β
βββββββββββββββββββββββββββββββββββββββββββββββββ
β β β
readβ reversibleβ irreversibleβ
βΌ βΌ βΌ
RUNS NOW approval, until ALWAYS
promoted APPROVAL
(never promotes)
β
βΌ
DOWNSTREAM MCP SERVERThe three tiers
Tier | Meaning | Behaviour |
| No side effects | Runs immediately, always |
| Real effects, can be undone | Gated β can earn autonomy |
| Cannot be undone | Gated permanently |
The hard rule: an irreversible tool can never be promoted to run unattended, regardless of how good its track record is. Trust is earned per tool, but the ceiling is set by consequence, not history. That single constraint is what makes NitroWatch safe to put in front of a production system.
Earned autonomy
A reversible tool that accumulates 3 clean approvals with zero denials becomes eligible for promotion. NitroWatch then offers β it never promotes itself. A human confirms with promote_tool, and can withdraw it instantly with revoke_tool.
A single denial permanently disqualifies a tool from autonomy. If a human ever said "no", the pattern isn't clean enough to stop asking.
Blast radius
Before approving, the human sees what the action would actually touch:
scoped to accountId="acc_991"
UNBOUNDED β "force" is set, so this may affect every record this tool can reach"Approve this?" and "approve this, knowing it hits every record" are very different questions.
β οΈ The bug we found in our own classifier
We pointed the classifier at a domain it had never been tuned for, and it immediately made a dangerous mistake. This is the most important thing we learned building NitroWatch, so it gets its own section rather than a footnote.
The classifier was written and tested against a billing vocabulary. To check whether it generalised, we ran it over an infrastructure server β cluster operations, databases, backups. One result stopped us:
β read scale_deployment matched read-only verb "count"scale_deployment was classified read.
In NitroWatch, read means autonomous from birth β it runs immediately, forever, with no approval. A tool that resizes production deployments would have run completely unsupervised.
Why it happened
The classifier read the tool name and its description as one bag of words. The description said:
"Change the replica count of a deployment."
count is in the read-verb list. One noun in a sentence of prose was enough to grant a mutating tool permanent unattended execution.
The same flaw hit rotate_credentials, which matched set in "invalidate the previous set" β again a noun, not a verb.
The two rules that came out of it
1. The tool name is authoritative. A name is written to state what a tool does. A description is prose. They are no longer read as one bag of words β the name decides the tier, and the description is consulted separately.
2. The description may only escalate risk β never reduce it.
Prose can legitimately reveal that a tool is more dangerous than its name suggests β a cleanup tool that "permanently deletes archived records" really is irreversible, and we want that caught. But a description can never argue a tool down to read.
And an unrecognised name with a read-looking description now defaults to reversible, not read. No evidence from the name is not enough to grant unattended execution.
π Rule 2 is a security property, not just a bug fix
If a description could lower a tool's tier, then the description becomes an attack surface. Anyone who controls the text of a tool β the author of a third-party MCP server you connected β could write prose designed to talk NitroWatch into treating delete_everything as read-only.
Under the current rules that is impossible. The worst a hostile description can do is make a tool look more dangerous than it is, which fails safe. The same principle governs the LLM classifier: on disagreement, the more dangerous verdict always wins.
What this cost, and what it bought
One test run against an unfamiliar vocabulary. It is now locked in by a regression test asserting scale_deployment can never be classified read, plus a generalisation test over the whole infrastructure vocabulary.
We would rather ship a governance tool that has been caught failing and fixed than one that has never been tested outside the domain it was written for.
Architecture
src/
βββ index.ts McpApplicationFactory bootstrap
βββ app.module.ts @McpApp root module
βββ health/system.health.ts health checks
βββ modules/nitrowatch/
βββ nitrowatch.module.ts registers all controllers
βββ nitrowatch.store.ts state: servers, policies, trust, audit
βββ nitrowatch.policy.ts risk classifier + blast radius
βββ nitrowatch.tools.ts register / discover / burn rate / glue
βββ nitrowatch.governance.tools.ts classify / request / approve / promote
βββ nitrowatch.resources.ts policies, pending, audit, logs
βββ nitrowatch.prompts.ts approval briefing, posture reviewDesign notes
nitrowatch.store.tsis the only stateful module. Everything else reads through its helpers, so swapping in a database touches exactly one file.All execution funnels through one function (
executeOnServer), so nothing can run without passing an audit point.The classifier is deterministic and biased toward caution. Anything it cannot confidently read as read-only is treated as at least
reversibleβ an over-cautious classifier costs a click, an under-cautious one costs a database.NitroWatch is an MCP server that is also an MCP client. It speaks the protocol in both directions, which is what lets it govern arbitrary servers.
Tools
Tool | Purpose |
| Register an MCP server to be governed |
| Connect to it and enumerate tools/resources/prompts |
| Assign every tool a risk tier |
| Agent asks to call a tool β allowed, or queued for approval |
| Human approves β executes, trust +1 |
| Human denies β blocked, autonomy disqualified |
| Grant standing autonomy after a clean record |
| Withdraw autonomy immediately |
| Track record and autonomy state per tool |
| Full decision history |
| Token budget projection |
| Stub connector between two registered servers |
Resources
URI | Contents |
| All registered servers |
| Per-server log entries |
| Risk tier + autonomy for every governed tool |
| Actions awaiting a human decision |
| Complete audit trail |
Prompts
Prompt | Purpose |
| Plain-language brief for a pending action |
| What runs unattended, what is gated, what looks risky |
Try it β the companion demo server
nitrowatch-billing-demo is an intentionally ungoverned MCP server built to be governed by this one. It exposes five tools that land on all three risk tiers β including a delete_account that will permanently destroy an account and its invoices for anyone who asks, with no confirmation.
git clone https://github.com/Mohith535/nitrowatch-billing-demo.git
cd nitrowatch-billing-demo && npm install && npm run build
npx nitrostack-cli start --port 3100Then, from NitroWatch:
register_server({ name: "Billing API", endpoint: "http://localhost:3100/sse" })
discover_capabilities({ serverId: "billing-api" })
classify_tools({ serverId: "billing-api" })
request_action({ serverId: "billing-api", toolName: "delete_account",
args: { accountId: "acc_991" } }) // β blockedβ οΈ Use
--portβ thePORTenvironment variable is silently ignored and the server would otherwise bind 3000, colliding with NitroWatch.If you run NitroWatch from NitroCloud rather than locally, it cannot reach
localhoston your machine. Run both locally, or deploy the billing server too and register its public URL.
Installation
Requirements: Node.js 20.x (18+ minimum), npm 9+
git clone https://github.com/Mohith535/nitrowatch.git
cd nitrowatch
npm install
npm run devThen open the project in NitroStudio β Studio App Canvas β Tools.
Production
npm run build # β dist/
npm run start:prodEnvironment setup
Copy .env.example to .env. No secrets are required to run locally.
Variable | Default | Purpose |
|
| Log verbosity |
|
| NitroStack app mode |
|
|
|
|
| HTTP port when transport is |
| unset | Enables LLM classification. Unset is a valid state β the deterministic classifier runs instead. |
|
| Any OpenAI-compatible endpoint (Groq, OpenRouter, Cerebras, local) |
|
| Model name for the above |
API keys for governed servers are supplied per-server via
register_serverand are never committed.
Testing
npm test18 tests over the security-critical logic β tier assignment, verb precedence, tokenization, the fail-safe default, blast-radius detection, description-escalation rules, cross-domain generalisation, and the promotion rules.
They earn their keep. They have caught three real bugs so far β the third is significant enough to have its own section above:
Conjugated verbs didn't match. A description reading "permanently deletes archived records" classified as
reversible, because the verb list helddeleteand the text saiddeletes. Fixed with suffix stripping β deliberately not prefix matching, which would makesettingsmatch the verbsetand misclassifyget_settings.camelCase argument keys were invisible to blast-radius detection.
{ accountId: "acc_991" }reported "scope inferred from: accountId" instead of the actual value, because the regex anchored on_or start-of-string. Keys are now tokenized the same way tool names are.β οΈ A mutating tool was classified read-only because a noun in its description matched a read verb β
scale_deploymentwould have run unattended. See the section above. This is the one that mattered.
All three were in code that looked obviously correct.
Classification: two layers
classify_tools tries the LLM classifier first and falls back to the deterministic one. Three rules govern the interaction:
Any failure falls back β no key, quota exhausted, timeout, malformed reply. Governance must never fail open.
On disagreement, the more dangerous tier wins. Disagreement is a signal to be careful, not a coin flip.
Rule 2 is also a prompt-injection defence. A hostile tool description cannot talk the system down from a tier the verb heuristic already flagged β the worst it can do is talk it up.
This mirrors the deterministic classifier's own rule that a description may only escalate risk β see the bug we found. Both layers fail in the same safe direction, deliberately.
The LLM call is capped at 8 seconds so a slow classifier can't stall the governance path.
Known limitations
Stated plainly, because a governance tool that hides its gaps is worth less than one that names them.
Governance decisions are unauthenticated. Anyone who can reach the server can call
approve_actionorpromote_tool. Production would gate these behind operator identity β NitroStack's@UseGuardsis the natural mechanism. This is the most significant gap.State is in-memory. A serverless cold start clears policies, approvals, and the audit trail.
nitrowatch.store.tsis the only file that would change.estimateBlastRadiusreads arguments, not the target system. It cannot know that{ status: "inactive" }matches 40,000 rows. It flags shape, not true magnitude.get_burn_ratetakes usage as an input rather than measuring it, so it projects rather than tracks.generate_glueemits a stub and does not solve schema mapping between the two tools.
Usage
A full governance cycle:
// 1. Put a server under governance
register_server({ name: "Billing API", endpoint: "https://billing.example.com/sse" })
discover_capabilities({ serverId: "billing-api" })
classify_tools({ serverId: "billing-api" })
// β get_invoice read runs freely
// β send_invoice reversible gated, can earn autonomy
// β delete_account irreversible gated forever
// 2. A read passes straight through
request_action({ serverId: "billing-api", toolName: "get_invoice", args: { id: "inv_1" } })
// β { decision: "allowed", tier: "read" }
// 3. Something dangerous is stopped
request_action({ serverId: "billing-api", toolName: "delete_account", args: { accountId: "acc_991" } })
// β { decision: "blocked", approvalId: "act_1", tier: "irreversible",
// blastRadius: 'scoped to accountId="acc_991"' }
// 4. Human decides, with context
approval_briefing({ approvalId: "act_1" })
deny_action({ approvalId: "act_1", reason: "not authorised for bulk deletion" })
// 5. A reversible tool earns its way up
request_action(...) β approve_action(...) // Γ3
// β promotionOffer: { eligible: true, ... }
promote_tool({ serverId: "billing-api", toolName: "send_invoice" })
// β now runs unattended
// 6. But the irreversible one never can
promote_tool({ serverId: "billing-api", toolName: "delete_account" })
// β Error: Refused β irreversible tools can never be promoted.Roadmap
LLM classification β
classifyWithLlm()innitrowatch.policy.tsis the seam. Contract: LLM first, deterministic fallback on any failure, and on disagreement take the more dangerous tier.Approval console widget β a React widget over
nitrowatch://pending.Durable state β swap the in-memory Maps in
nitrowatch.store.tsfor a database so policies survive a serverless cold start.
License
Apache-2.0
Available Tools
14 toolsapprove_actionA
Human approves a pending action. Executes it, records the approval, and reports whether the tool has now earned promotion.
Inputs: approvalId (required).
classify_toolsA
Assign a risk tier (read / reversible / irreversible) to every tool on a registered server. Run this after discover_capabilities. Tries the LLM classifier first and falls back to the deterministic one.
Inputs: serverId (required).
deny_actionA
Human denies a pending action. Nothing executes, and the denial is recorded permanently against that tool.
Inputs: approvalId (required), reason.
discover_capabilitiesB
Connect to a registered server's real MCP endpoint and list its tools/resources/prompts
Inputs: serverId (required).
generate_glueB
Generate a stub connector function that calls a tool on one registered server and pipes its result into a tool on another
Inputs: sourceTool (required), targetTool (required), sourceServerId (required), targetServerId (required).
get_audit_logB
Return the full decision history: what was classified, allowed, blocked, approved, denied, promoted, and by whom.
Inputs: limit, serverId.
get_burn_rateB
Project time-to-exhaustion on the team's 5M AI token budget
Inputs: used (required), budgetTotal.
get_trust_statusA
Show the approval record and autonomy state for every governed tool β the evidence behind each promotion decision.
Inputs: serverId.
list_pending_approvalsA
Show every action currently blocked and waiting on a human decision, newest first. Renders the interactive approval console.
Inputs: serverId.
promote_toolB
Grant a reversible tool standing permission to run unattended. Requires a clean approval record. Irreversible tools are refused.
Inputs: serverId (required), toolName (required).
register_serverB
Register an MCP server with NitroWatch so it can be watched
Inputs: name (required), endpoint (required), apiKey.
request_actionA
Ask NitroWatch to run a tool on a governed server. Read tools and promoted tools run immediately; everything else is queued for human approval and returns an approvalId.
Inputs: serverId (required), toolName (required), args.
revoke_toolA
Withdraw a tool's autonomy immediately. It returns to requiring approval on every call.
Inputs: serverId (required), toolName (required).
set_tierA
Manually override a tool's risk tier when the classifier gets it wrong. Recorded as a human decision in the audit trail.
Inputs: tier (required), reason (required), serverId (required), toolName (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelogβ¦
TDQS
Scored across 14 tools
Every tool targets a distinct function: registration, discovery, classification, override, request, approval listing, approve, deny, promote, revoke, trust status, audit log, budget, and glue generation. No two tools overlap in purpose.
All tool names follow a consistent verb_noun pattern (e.g., register_server, classify_tools, approve_action). No mixed casing or inconsistent verb styles.
14 tools is well within the ideal 3-15 range and each tool covers a distinct step in the governance workflow. The count feels justified by the breadth of the domain.
The core lifecycle is well covered: register, discover, classify, request, approve, promote, revoke, audit. Missing server removal or listing (e.g., unregister_server) is a minor gap, but agents can work around it.
Maintenance
Related MCP Connectors
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Identity, authorization, audit trails, and revocable permissions for AI agents accessing MCP tools.
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
Zero-secret MCP gateway for AI agents: risk-scored, audited calls with human-in-the-loop approval.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA governance and control layer for MCP tools that manages tool requests as intents through policy-based approval, queuing, or blocking. It enables secure human oversight and audit trails for consequential agent actions across platforms like Claude Desktop and Cursor.1MIT No Attribution

Oakallow MCP Serverofficial
AlicenseNot gradedqualityBmaintenanceRuntime permission, approval, and audit governance for AI agent tool execution, enabling human oversight of risky actions via an MCP server.1MIT- AlicenseNot gradedqualityDmaintenanceA policy-enforcing MCP gateway that intercepts all tool calls to downstream MCP servers, applying allow/deny/ask rules with human approval and audit logging for safe access to dangerous tools.10 npmMIT
- FlicenseNot gradedqualityDmaintenanceMCP server that provides a security gateway for AI agents, enforcing allow/confirm/deny policies on tool calls and requiring human approval for risky operations, with full audit logging.-