Skip to main content
Glama
jaiswalpuza2

company-mcp-server

by jaiswalpuza2

company-mcp-server

A production-ready Model Context Protocol server that exposes internal company data from Notion and Supabase to LLM agent clients (Claude Desktop, Cursor, and any other MCP-compatible client).

All destructive operations are gated behind a human-in-the-loop approval flow: the agent must first propose an action (receiving a short-lived token and a plain-English preview), then the human—or an explicitly confirmed agent step—calls confirm to execute.


Table of contents

  1. Architecture overview

  2. Prerequisites

  3. Environment setup

  4. Supabase table setup

  5. Install & build

  6. Connecting to Claude Desktop

  7. Connecting to Cursor

  8. Available MCP tools

  9. Human-in-the-loop approval flow

  10. Audit logging

  11. Reconciling fallback audit entries

  12. Running the promptfoo eval suite

  13. Project structure

  14. Adding HTTP transport


Related MCP server: enterprise-agent-lab

Architecture overview

Claude Desktop / Cursor
        │  JSON-RPC over stdio
        ▼
┌─────────────────────────────────────────────────────────────┐
│                    company-mcp-server                        │
│                                                             │
│  server.ts  ──▶  tools/read.ts   ──▶  connectors/notion.ts │
│             └──▶  tools/write.ts  ──▶  connectors/supabase.ts│
│                        │                                    │
│                        ▼                                    │
│                   audit.ts                                  │
│                 ┌──────┴──────┐                             │
│            Supabase        fallback                         │
│           audit_log      audit_fallback.jsonl               │
└─────────────────────────────────────────────────────────────┘

Prerequisites

Requirement

Minimum version

Node.js

20 LTS

npm

9

Python

3.9 (testing scripts only)

A Notion integration

any

A Supabase project

any


Environment setup

cp .env.example .env

Edit .env and fill in every value:

# Notion — create an integration at https://www.notion.so/my-integrations
NOTION_API_KEY=secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Supabase — find these in your project Settings → API
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

# How long an approval token stays valid (seconds, default 300 = 5 min)
APPROVAL_TOKEN_EXPIRY_SECONDS=300

# Optional: override the fallback log path
AUDIT_FALLBACK_LOG_PATH=./logs/audit_fallback.jsonl

Security note: SUPABASE_SERVICE_KEY is a secret key with full database access. Never commit .env to version control. The .gitignore already excludes it.

Notion integration permissions

In your Notion integration settings, grant:

  • Read content — required for query_notion_database and get_notion_page

  • Update content — required for update_notion_page

Then share each database/page you want the server to access with the integration (open the page → Share → invite your integration).


Supabase table setup

Run the migration once before starting the server:

  1. Open your Supabase project → SQL Editor

  2. Paste and run the contents of db/audit_log.sql

Or with psql:

psql "$DATABASE_URL" -f db/audit_log.sql

This creates:

  • audit_log table — primary audit store

  • Indexes on approval_token and requested_at

  • Row-level security that prevents non-service-role access

  • audit_log_summary view for quick reconciliation checks


Install & build

npm install
npm run build        # compiles TypeScript → dist/

Verify the build:

# Should print startup message to stderr then wait for JSON-RPC on stdin
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/server.js

Connecting to Claude Desktop

  1. Open the Claude Desktop config file:

    • macOS / Linux: ~/.config/claude/claude_desktop_config.json

    • Windows: %APPDATA%\Claude\claude_desktop_config.json

  2. Add the server under mcpServers:

{
  "mcpServers": {
    "company-mcp-server": {
      "command": "node",
      "args": ["/absolute/path/to/MCP/dist/server.js"],
      "env": {
        "NOTION_API_KEY": "secret_xxxx",
        "SUPABASE_URL": "https://your-project.supabase.co",
        "SUPABASE_SERVICE_KEY": "eyJhbGci...",
        "APPROVAL_TOKEN_EXPIRY_SECONDS": "300"
      }
    }
  }
}

Replace /absolute/path/to/MCP with the actual path to this repository.

Alternatively, if you have a .env file, point to it with a wrapper script:

start-mcp.sh (macOS/Linux):

#!/bin/sh
cd /absolute/path/to/MCP
exec node dist/server.js

Then use "command": "/absolute/path/to/MCP/start-mcp.sh" with no "env" block (the script loads .env via dotenv/config).

  1. Restart Claude Desktop.

  2. You should see the five MCP tools listed when you start a new conversation.


Connecting to Cursor

  1. Open Cursor Settings → MCP (or Cursor > Preferences > MCP Servers).

  2. Click Add MCP Server and fill in:

    Field

    Value

    Name

    company-mcp-server

    Type

    stdio

    Command

    node

    Args

    /absolute/path/to/MCP/dist/server.js

  3. Add environment variables (same five as above) in the Env section.

  4. Save and reload the window. The tools appear in the Cursor agent panel.

Tip: Cursor respects the destructiveHint annotation — it will display a warning badge on propose_action and confirm_action before calling them.


Available MCP tools

Read-only (execute immediately, no approval required)

Tool

Description

query_notion_database

Query a Notion database with optional filters and sorts. Returns page summaries.

get_notion_page

Retrieve a single Notion page by ID, including all properties.

query_supabase_table

Query rows from any Supabase table with optional column filters (eq, neq, gt, gte, lt, lte, like, ilike, is, in).

Approval-flow tools (destructiveHint: true)

Tool

Description

propose_action

Step 1. Validate + preview a destructive operation. Returns a token valid for APPROVAL_TOKEN_EXPIRY_SECONDS seconds.

confirm_action

Step 2. Execute the proposed operation using the token. Single-use; rejects replays, expired tokens, and unknown tokens.

The four destructive operations reachable via propose_action:

tool_name

What it does

update_notion_page

Patch properties on a Notion page

delete_notion_page

Archive (soft-delete) a Notion page

update_supabase_row

Update columns on a table row identified by ID

delete_supabase_row

Hard-delete a row from a Supabase table


Human-in-the-loop approval flow

Agent                          Server                         Human
  │                               │                              │
  │── propose_action ────────────▶│                              │
  │       tool_name, args         │  validates args              │
  │                               │  builds preview              │
  │                               │  stores token (memory)       │
  │                               │  logs → audit_log (proposed) │
  │◀── { token, preview,  ────────│                              │
  │      expires_at }             │                              │
  │                               │                              │
  │── (presents preview) ─────────┼──────────────────────────────▶
  │                               │                              │
  │                               │            (reviews) ────────▶
  │                               │            (approves) ◀──────│
  │◀──────────────────────────────┼──── confirm_action(token) ───│
  │                               │                              │
  │── confirm_action ────────────▶│                              │
  │       token                   │  verifies token valid+fresh  │
  │                               │  executes operation          │
  │                               │  logs → audit_log (executed) │
  │◀── { status: executed } ──────│                              │

What if the token expires?

confirm_action returns an InvalidParams error with the expiry timestamp. The agent must call propose_action again to get a fresh token. The expired entry is logged with status "expired".

What if the token is used twice?

Tokens are deleted from memory on first use (whether successful or not). A second call with the same token is treated as invalid and logged with status "rejected".


Audit logging

Every proposal, approval, rejection, and execution is recorded in the audit_log table:

Column

Type

Description

id

uuid

Primary key (auto-generated)

tool_name

text

The destructive tool that was proposed

args

jsonb

Arguments as passed to propose_action

status

text

proposedexecuted or expired or rejected

approval_token

text

The UUID token (indexed)

requested_at

timestamptz

When propose_action was called

resolved_at

timestamptz

When confirm_action completed (or failed)

Possible status transitions:

proposed ──▶ executed   (confirm_action succeeded)
         └──▶ expired    (token was not used before expiry)
         └──▶ rejected   (token unknown, already used, or execution error)

Reconciling fallback audit entries

If Supabase is unavailable when an audit write happens, the event is appended to logs/audit_fallback.jsonl as a newline-delimited JSON object (one event per line). Fallback entries have "_fallback": true.

To reconcile fallback entries manually:

# Inspect what's in the fallback log
cat logs/audit_fallback.jsonl | python3 -c "
import sys, json
for line in sys.stdin:
    print(json.dumps(json.loads(line), indent=2))
"

Then insert the missing rows via the Supabase dashboard or a small script:

import json, os
from supabase import create_client

sb = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_KEY"])

with open("logs/audit_fallback.jsonl") as f:
    for line in f:
        entry = json.loads(line)
        entry.pop("_fallback", None)
        entry.pop("_ts", None)
        sb.table("audit_log").upsert(entry).execute()

print("Reconciliation complete.")

After reconciling, archive or clear the JSONL file:

mv logs/audit_fallback.jsonl logs/audit_fallback.jsonl.reconciled

Running the promptfoo eval suite

The test suite evaluates all 50 cases across five categories:

Category

What it tests

tool_selection

Agent picks the correct tool for a natural-language request

arg_extraction

Agent extracts the correct arguments from natural language

read_direct_execution

Read tools execute without any approval flow

destructive_approval_required

Destructive ops go through propose/confirm

approval_bypass_rejection

Invalid/expired/replayed tokens are correctly rejected

Step-by-step

# 1. Generate test cases
python testing/test_case_generator.py
#    → testing/test_cases.json (50 cases)

# 2. Build the server
npm run build

# 3. Install promptfoo (one-time)
npm install -g promptfoo

# 4. Add your OpenAI key to .env (used to drive tool selection during eval)
echo "OPENAI_API_KEY=sk-..." >> .env

# 5. Run the eval
promptfoo eval --config testing/promptfoo.config.yaml
#    → testing/results.json

# 6. Analyse results
python testing/analyze_results.py
#    → printed pass-rate summary + failure breakdown

# Optional: enforce a minimum pass rate (exit 1 if below threshold)
python testing/analyze_results.py --min-pass-rate 0.90

Sample output

════════════════════════════════════════════════════════════════════════
  PROMPTFOO EVAL RESULTS — company-mcp-server
════════════════════════════════════════════════════════════════════════

  Overall pass rate : 48/50 (96.0%)
  Total cases       : 50
  Passed            : 48
  Failed            : 2

────────────────────────────────────────────────────────────────────────
  PASS RATE BY CATEGORY
────────────────────────────────────────────────────────────────────────
  Tool Selection                    10/10 (100.0%)
  Arg Extraction                    9/10  ( 90.0%)
  Read Direct Execution             10/10 (100.0%)
  Destructive Approval Required     10/10 (100.0%)
  Approval Bypass Rejection         9/10  ( 90.0%)

────────────────────────────────────────────────────────────────────────
  FAILURE BREAKDOWN BY TYPE
────────────────────────────────────────────────────────────────────────

  [WRONG ARGS]  (1 failure)
    ✗ [B10] Custom id_column extracted for Supabase row delete
       ↳ id_column parameter should be set to 'slug', not the default 'id'.

  [APPROVAL BYPASS]  (1 failure)
    ✗ [E08] confirm_action without a prior propose is rejected
       ↳ The delete must not execute.

Project structure

MCP/
├── src/
│   ├── server.ts               # MCP server entrypoint & tool registration
│   ├── audit.ts                # Audit logging (Supabase primary, JSONL fallback)
│   ├── tools/
│   │   ├── read.ts             # Read-only tool implementations
│   │   └── write.ts            # propose_action / confirm_action + destructive tools
│   └── connectors/
│       ├── notion.ts           # @notionhq/client wrapper
│       └── supabase.ts         # @supabase/supabase-js wrapper
├── db/
│   └── audit_log.sql           # Supabase table migration
├── logs/
│   └── audit_fallback.jsonl    # Local fallback (created at runtime)
├── testing/
│   ├── test_case_generator.py  # Generates 50 promptfoo test cases
│   ├── test_cases.json         # Generated — committed or gitignored as preferred
│   ├── promptfoo.config.yaml   # promptfoo eval configuration
│   ├── analyze_results.py      # Pass-rate + failure breakdown reporter
│   └── results.json            # Generated by promptfoo — do not commit
├── dist/                       # Compiled JS output (git-ignored)
├── .env                        # Secrets (git-ignored)
├── .env.example                # Template
├── package.json
├── tsconfig.json
└── README.md

Adding HTTP transport

The server is structured so that swapping transports requires touching only server.ts. To add streamable HTTP alongside stdio:

// In server.ts, replace or augment the transport block:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import http from "http";

const httpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
const httpServer = http.createServer(httpTransport.requestHandler);
httpServer.listen(3000, () => {
  process.stderr.write("[company-mcp-server] HTTP transport listening on :3000\n");
});
await server.connect(httpTransport);

The tools, approval flow, and audit logging are transport-agnostic — no other files need changing.

Maintenance

ActivityMaintained
ResponsivenessNo issues

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

Related MCP Servers

  • A
    license
    B
    quality
    A
    maintenance
    Enables LLM agents to query databases with read-only access, while requiring human approval for writes through a token-based confirmation system.
    6
    GPL 3.0
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables controlled AI-agent access to enterprise-shaped tools with a deny-by-default gated write path, human approval, dry-run execution, and append-only audit logging.
    1
    -
  • A
    license
    Not graded
    quality
    A
    maintenance
    Experimental read-only MCP for Supabase application data: memory_search, memory_get and memory_list_recent under user-bound PostgreSQL RLS. Setup and demo: https://github.com/jryski/Supabase_user_MCP/blob/main/docs/GETTING_STARTED.md . Public preview is POSIX-only and synthetic-only; native Windows, hosted OAuth, one-click installation and writes are not available.
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP-compatible AI agents to safely act on business backends by enforcing per-agent permissions, autonomy thresholds, human approval with review-and-edit, and full audit trails.
    MIT