@nozomtechs/grc-mcp
v1.1.0
Published
MCP server for the Nozom Cybersecurity GRC Platform — exposes GRC API endpoints as Model Context Protocol tools. R-EXTSPEC: @modelcontextprotocol/sdk v1.29.0 — https://modelcontextprotocol.io/specification/latest
Readme
grc-mcp — Nozom GRC MCP Server
MCP (Model Context Protocol) server for the Nozom Cybersecurity GRC Platform. Exposes the GRC API as MCP tools consumable by Claude Desktop, Cursor, and other MCP clients.
Spec: @modelcontextprotocol/sdk v1.29.0 — https://modelcontextprotocol.io/specification/latest Backend: Nozom GRC API (grc-backend) — authenticated via Personal Access Token (PAT).
Quickstart
Zero-install (recommended): the MCP client launches grc-mcp on demand via npx. No global install needed; npm fetches the package the first time and caches it.
Or install globally for repeat use without npx overhead:
npm install -g @nozomtechs/grc-mcp1. Create a scoped PAT
In the GRC web app: Profile → API → Create Token.
Select scope: MCP read-only, MCP read+write, or MCP admin depending on what you need.
Copy the grc_… token immediately — it is shown only once.
2. Configure your MCP client
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"grc": {
"command": "npx",
"args": ["-y", "@nozomtechs/grc-mcp", "--backend-url", "https://grc-api.nozomtechs.sa"],
"env": {
"GRC_PAT": "grc_<your-token-here>"
}
}
}
}Cursor (.cursor/mcp.json)
{
"mcpServers": {
"grc": {
"command": "npx",
"args": ["-y", "@nozomtechs/grc-mcp", "--backend-url", "https://grc-api.nozomtechs.sa"],
"env": {
"GRC_PAT": "grc_<your-token-here>"
}
}
}
}If you
npm install -g @nozomtechs/grc-mcpinstead, replace"command": "npx"and"args": ["-y", "@nozomtechs/grc-mcp", ...]with"command": "grc-mcp"and"args": [...](the rest stays the same).
4. Verify
In Claude Desktop or Cursor, ask: "List the available GRC tools." — the tool list should appear.
CLI Options
| Flag | Default | Description |
|------|---------|-------------|
| --backend-url <url> | http://localhost:5282 | GRC API base URL |
| --transport <stdio\|http> | stdio | Transport type (stdio or http) |
| --port <N> | 3000 | HTTP listen port (HTTP transport only) |
| --host <H> | 127.0.0.1 | HTTP bind address (HTTP transport only; loopback by default — AI-58) |
| --cors-origin <origin> | (none) | Allowed CORS origin for browser clients (e.g. https://app.example.com; * = all origins, insecure) |
| --smoke-test | — | Validate PAT + list tools, then exit 0 or 1 |
| --cache-ttl <seconds> | 300 | PAT validation cache TTL (0 = disabled) |
| --scope <read\|write\|admin> | (PAT governs) | Server-level scope downgrade. Effective scope = min(PAT scope, this flag). |
| --readonly | — | Alias for --scope=read (shorthand for read-only AI agent configs). |
| --no-confirm-delete | — | Disable the confirm-delete gate (see Safety Rails below). |
| --rate-limit <N> | 60 | Max tool calls per minute (0 = disabled). Token bucket per session/PAT. |
| --include-modules <csv> | (all) | Comma-separated list of modules to expose (e.g. Risks,Findings). |
| --preset <name> | (all) | Curated module group (see Safety Rails below). Mutually exclusive with --include-modules. |
Environment variables (override CLI flags):
| Variable | Description |
|----------|-------------|
| GRC_PAT | PAT token (grc_…) — required for stdio; not required for HTTP (PAT comes per-connection via Authorization: Bearer) |
| GRC_BACKEND_URL | Backend URL — optional |
Safety Rails
Story 32-5 adds four orthogonal safety layers that narrow the tool set and call budget without touching auth or transport.
1. Server-level scope downgrade (--scope / --readonly)
The --scope flag downgrades the effective scope below what the PAT allows.
Effective scope = min(PAT scope, --scope flag) — the flag can only restrict, never elevate.
# Read-only mode (even if PAT has write scope)
grc-mcp --readonly --backend-url https://grc-api.nozomtechs.sa
# Equivalent
grc-mcp --scope=read --backend-url https://grc-api.nozomtechs.sa| PAT scope | --scope flag | Effective scope |
|-----------|---------------|-----------------|
| mcp:write | read | mcp:read (flag wins) |
| mcp:read | write | mcp:read (PAT wins) |
| null (full) | read | mcp:read (server downgrade) |
| null (full) | admin | null (admin flag + null PAT = no restriction) |
Unknown --scope values print an error and exit 1.
2. Confirm-delete gate (--no-confirm-delete)
By default, every DELETE tool requires { confirm: true } in its arguments to execute.
The tool description shows: ⚠️ DESTRUCTIVE: Set confirm=true to execute this delete.
If an AI agent calls a DELETE tool without confirm: true, the MCP server returns an error immediately — no HTTP request is sent to the backend.
# Disable the confirm gate (e.g. when scripting known-safe automated runs)
grc-mcp --no-confirm-delete --backend-url https://grc-api.nozomtechs.saAI-58: The confirm field value is never logged.
3. Rate limiting (--rate-limit)
Token bucket rate limiter: default 60 tool calls per minute.
- stdio mode: one bucket per process (stdio is single-PAT by design — AI-58: no full token in bucket scope).
- HTTP mode: one bucket per session (created at session init; reclaimed when the session's MCP server closes).
- Exceeding the limit returns a rate-limit error with
Retry after Ns.— no HTTP request sent. - Invalid
--rate-limitvalues (non-integer, negative) exit withprocess.exit(1).
# Custom rate limit
grc-mcp --rate-limit=120 --backend-url https://grc-api.nozomtechs.sa
# Disable rate limiting
grc-mcp --rate-limit=0 --backend-url https://grc-api.nozomtechs.sa4. Module filter (--include-modules / --preset)
Restrict which modules' tools are registered. Useful when an AI agent only needs a subset (e.g. risk-only for a risk analyst agent).
# Only Risks and Findings modules
grc-mcp --include-modules=Risks,Findings --backend-url https://grc-api.nozomtechs.sa
# Use a preset
grc-mcp --preset=risk --backend-url https://grc-api.nozomtechs.saAvailable presets:
| Preset | Modules |
|--------|---------|
| risk | Risks, Methodologies |
| compliance | Frameworks, Controls, Compliance |
| audit | Audit |
| bcm | Bcm |
| phishing | Phishing |
| core | Risks, Findings, Tasks, Exceptions |
--preset and --include-modules are mutually exclusive (error if both set).
Startup log confirms what's being exposed:
[grc-mcp] Exposing 80 tools across 4 modules: Exceptions, Findings, Risks, TasksExample: read-only risk analyst agent
{
"mcpServers": {
"grc-risks": {
"command": "grc-mcp",
"args": [
"--backend-url", "https://grc-api.nozomtechs.sa",
"--readonly",
"--preset=risk",
"--rate-limit=30"
],
"env": {
"GRC_PAT": "grc_<mcp-read-token>"
}
}
}
}HTTP Transport
Story 32-4 adds a second transport — Streamable HTTP — per the MCP 2025-11-25 specification. Use HTTP transport for cloud AI agents, server-to-server MCP calls, or HTTP clients.
Starting in HTTP mode
# Bind to loopback (default — safe for local use)
grc-mcp --transport=http --backend-url https://grc-api.nozomtechs.sa
# Bind to a specific interface (non-loopback — ensure network-level access controls)
grc-mcp --transport=http --host 0.0.0.0 --port 4000 --backend-url https://grc-api.nozomtechs.sa
# With CORS for browser-based clients
grc-mcp --transport=http --cors-origin https://app.example.com --backend-url https://grc-api.nozomtechs.saHTTP endpoint
All MCP requests go to POST /mcp (or GET /mcp for SSE streams, DELETE /mcp for session termination).
Auth in HTTP mode
In HTTP mode, PAT comes from the Authorization: Bearer <PAT> header of each initialization request:
POST /mcp HTTP/1.1
Authorization: Bearer grc_<your-token>
Content-Type: application/json
Accept: application/json, text/event-streamGRC_PATenv var is NOT required in HTTP mode (a warning is logged if it is set).- Each connection validates its PAT independently — sessions with different PATs are fully isolated.
Example curl flow
# 1. Initialize — get a session ID
curl -X POST http://127.0.0.1:3000/mcp \
-H "Authorization: Bearer grc_<your-token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}},"id":1}'
# The response includes: MCP-Session-Id: <uuid>
# 2. List tools (use the session ID from the initialize response)
curl -X POST http://127.0.0.1:3000/mcp \
-H "MCP-Session-Id: <uuid-from-step-1>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":2}'
# 3. Call a tool
curl -X POST http://127.0.0.1:3000/mcp \
-H "MCP-Session-Id: <uuid-from-step-1>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"grc.list_risks","arguments":{"pageSize":5}},"id":3}'
# 4. Terminate session
curl -X DELETE http://127.0.0.1:3000/mcp \
-H "MCP-Session-Id: <uuid-from-step-1>"Generic HTTP MCP client config
{
"mcpServers": {
"grc": {
"transport": "http",
"url": "http://127.0.0.1:3000/mcp",
"headers": {
"Authorization": "Bearer grc_<your-token>"
}
}
}
}Security
- Default bind is loopback (
127.0.0.1) — only local processes can connect. Pass--host 0.0.0.0to expose remotely (logged as SECURITY WARNING). - Origin validation per MCP spec: non-loopback
Originheaders are rejected with403unless--cors-originis configured. - PAT never logged — log lines include only session ID prefix, HTTP status code, and endpoint path.
- 401 responses contain only
{"error": "Unauthorized"}— no token, no Bearer prefix in the body.
Architecture
- Transports: stdio (Story 32-2) and Streamable HTTP (Story 32-4).
- Auth: PAT validated via
GET /v1/users/me/pat-info; cached per session (5 min TTL). - Tool registry: Generated from
manifest/api-manifest.jsonat startup (Story 32-3). - Scope enforcement:
pat_scopeclaim checked —mcp:readfor GET tools,mcp:writefor mutating tools,mcp:adminfor cross-tenant tools.
stdio connection lifecycle
- MCP client launches
grc-mcpas a subprocess (stdio transport). src/index.tsparses CLI args → readsGRC_PATenv var.src/server.tscreatesMcpServer+StdioServerTransport, callsserver.connect(transport).- At startup,
src/auth.tsvalidates the PAT via backend/v1/users/me/pat-info. - If valid: PAT info cached 5 min. Tools registered per PAT scope.
- If invalid/expired/revoked: server exits 1 at startup.
- Client disconnects →
StdioServerTransportcloses → process exits.
HTTP connection lifecycle
src/index.tsstartssrc/http-server.ts→http.createServer()binds to--host:--port.- Client sends
POST /mcpwithAuthorization: Bearer <PAT>+InitializeRequestbody. - Server validates PAT via
src/auth.ts→ if invalid, returns401(body:{"error":"Unauthorized"}). - If valid: new
McpServer+StreamableHTTPServerTransportcreated per session; tools registered with PAT's scope. - SDK assigns
MCP-Session-Id: <uuid>in the initialize response header. - Subsequent requests include
MCP-Session-Idheader → routed to the correct session's transport. - Client sends
DELETE /mcpwith session ID → session cleaned up;McpServer+ transport released. - Multiple simultaneous sessions (with different PATs) are supported — each fully isolated.
Branch Policy
master— demo/release branch. Direct commits only at demo cuts.feature/epic-{N}-{slug}— feature branches (base off master). Used during epic development.- Single-branch model: no integration branch needed (npm release cadence, no dev environment).
Security
- PAT never logged.
AI-58: thegrc_…plaintext token is read fromGRC_PATenv var, used as anAuthorization: Bearerheader only, and never written to stderr, any log file, or error message. - Scope enforced on both sides: backend
PatScopePreProcessor(Story 32-1) enforces scope; MCP server enforces scope as a defense-in-depth layer (Story 32-5). - Cache invalidated on revoke: if backend returns 401, PAT cache entry is immediately removed.
Development
git clone https://git.nozomtechs.sa/software-practice/grc-mcp.git
cd grc-mcp
npm install
npm run build
GRC_PAT=grc_<token> GRC_BACKEND_URL=http://localhost:5282 npm run test:smoke