notion-mcp-hono
Provides tools to search, retrieve, create, and update Notion pages and databases, enabling AI agents to manage Notion content programmatically.
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., "@notion-mcp-honosearch for pages about project roadmap"
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.
notion-mcp-hono
A remote MCP server exposing Notion as tools — TypeScript, Hono, the official MCP TypeScript SDK over Streamable HTTP, deployed on Railway.
Notion ships an official MCP server; this project is not trying to replace it. It is a portfolio piece demonstrating how to build a remote MCP server properly: focused scope, test-driven, authenticated, observable, and deployed.
Live demo
notion-mcp-hono-production.up.railway.app —
the root URL of the MCP endpoint doubles as a landing page with a live playground:
pick a read-only tool (search_pages, get_page, query_database), fill a form
generated from the tool's actual zod schema, and run it against a dedicated demo
workspace. Next to each result you see the JSON-RPC payload an MCP client would send.
The playground's /demo/* routes hold credentials server-side, expose the read-only
tools only (the write tools have no route entry — no input reaches them), and are
rate-limited per IP (10 requests/minute).
Related MCP server: Notion MCP Server
Tools
Tool | What it does |
| Search pages across the workspace by title |
| Retrieve one page as clean markdown |
| Create a page under a parent page or database |
| Append markdown to the end of an existing page |
| Query a database with Notion filters and sorts, rows simplified to flat JSON |
All tool inputs are validated with Zod (with size limits), all descriptions are written for the LLM consuming them, and all Notion failures come back as actionable, in-band error messages — never raw API errors.
Architecture
┌──────────────────────────────────────────────┐
MCP client │ notion-mcp-hono │
(claude.ai / │ │
Claude Code) │ Transport layer MCP layer │
│ │ ┌───────────────┐ ┌───────────────┐ │
│ POST /mcp │ │ Hono app │ │ McpServer │ │ Notion API
├─────────────────►│ │ bearer auth ├──────►│ 5 tools │ │
│ Authorization: │ │ body limit │ │ zod schemas │ │
│ Bearer <key> │ │ req logging │ │ tool runner │ │
│ │ └───────────────┘ └───────┬───────┘ │
│ GET /health │ │ │ │
├─────────────────►│ 200, no auth ┌────────▼────────┐ │
│ │ │ Notion layer │ │ ┌─────────┐
│ │ │ NotionGateway ├──┼──►│ Notion │
│ │ │ (interface) │ │ └─────────┘
│ │ │ SDK v5 adapter │ │
│ │ └─────────────────┘ │
└──────────────────┴──────────────────────────────────────────────┘Three layers, dependency arrows pointing inward:
Transport (
src/app.ts,src/auth.ts) — Hono routes, bearer auth, body limit, request logging.MCP (
src/mcp/) — server setup, the 5 tool registrations, the tool runner (error handling + per-call logging).Notion (
src/notion/) — theNotionGatewayinterface (domain types only) and its@notionhq/clientadapter. The MCP layer never imports the Notion SDK, so the integration is swappable without touching tool code.
Quickstart (local)
git clone <this repo> && cd notion-mcp-hono
npm install
cp .env.example .env # then fill in the two variables below
npm run dev # http://localhost:3000You need two secrets in .env (or exported):
Variable | What it is |
| An internal integration token from notion.so/my-integrations. Share the pages/databases you want accessible with that integration (page → ⋯ → Connections). |
| The bearer key clients of this server must present. Generate one: |
Check it's alive:
curl http://localhost:3000/health
# {"status":"ok"}
curl -s -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer $MCP_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Authentication
Every request to /mcp requires:
Authorization: Bearer <MCP_API_KEY>Comparison is timing-safe (both sides are SHA-256-hashed, then
crypto.timingSafeEqual— neither content nor length leaks through response timing).Missing/wrong token →
401with a JSON body naming the expected header.GET /healthis public (it's the deployment health check).
Connecting from Claude
claude.ai custom connector
Settings → Connectors → Add custom connector:
URL:
https://<your-deployment>.up.railway.app/mcpAdvanced settings → HTTP headers: add
Authorization=Bearer <MCP_API_KEY>
Claude Code
claude mcp add --transport http notion-hono \
https://<your-deployment>.up.railway.app/mcp \
--header "Authorization: Bearer <MCP_API_KEY>"Then /mcp inside Claude Code shows the connection; the five tools appear as
mcp__notion-hono__*.
Deploy on Railway
Create a project from this repo (Railway dashboard → New Project → Deploy from GitHub repo). The default Nixpacks build works as-is:
npm ci && npm run build && npm start.Set the environment variables on the service:
NOTION_TOKEN— your Notion internal integration tokenMCP_API_KEY— the bearer key for clients (generate:openssl rand -hex 32)DEMO_NOTION_TOKEN(optional) — a second integration token for a dedicated demo workspace, powering the public playground at/demo/*. Leave it unset to disable the playground (its routes then respond503); never point it at a workspace with real data.
PORTis injected by Railway automatically; the server reads it.Set the service health check path to
/health.Generate a public domain (service → Settings → Networking) and use
https://<domain>/mcpas the connector URL.
The server fails fast at boot with a clear message if either env var is missing.
Smoke test (manual, not CI)
Exercises a running server end-to-end against a real Notion workspace:
MCP_URL=https://<your-deployment>.up.railway.app \
MCP_API_KEY=<key> \
SMOKE_PARENT_PAGE_ID=<page id> # optional: enables create_page + append_blocks \
SMOKE_DATABASE_ID=<database id> # optional: enables query_database \
npm run smokeIt verifies /health, the 401 on unauthenticated /mcp, the initialize handshake,
tools/list, and each tool (write tools only when their env var is set — pages it
creates are titled Smoke test <timestamp> and safe to delete).
Development
npm test # Vitest, all Notion calls mocked — no network
npm run test:watch
npm run typecheck # strict, includes tests and scripts
npm run build # compiles src/ to dist/Tests run at three altitudes: pure functions (property simplification, auth, logger),
the MCP layer via InMemoryTransport with a fake gateway, and full HTTP integration
using the real MCP SDK client against the Hono app on an ephemeral port.
Design decisions
Streamable HTTP, stateless mode. This is a remote server, so stdio is out. Of Streamable HTTP's two modes we run stateless: a fresh
McpServer+ transport per request, noMcp-Session-Id, no session map. Every tool is pure request/response, so sessions would only buy horizontal-scaling problems. ConsequentlyGET/DELETE /mcpreturn405(nothing to stream or terminate).Markdown conversion is delegated to Notion. Notion's 2025-09-03 API has native markdown endpoints (
pages.retrieveMarkdown,pages.updateMarkdown). Hand-rolling a blocks↔markdown converter is an entire class of fidelity bugs this project chose not to own.NotionGatewayinterface as the seam. MCP tools depend on a 5-method interface speaking domain types (markdown strings, flat property values). The@notionhq/clientadapter — including thedatabase_id→ data-source resolution the 2025 API requires — is invisible to tool code and replaceable without touching it.Errors are a UX surface for the model. Zod validation failures and Notion errors return in-band (
isError: true) with messages that say what to fix ("share the page with the integration via ⋯ → Connections"), so the calling LLM can self-correct. Unexpected errors return a generic in-band message and are logged server-side — raw internals never reach the client.Auth is boring on purpose. One static bearer key, timing-safe compare, injected via
createApp({ mcpApiKey })so tests never touchprocess.env. OAuth is the spec's long-term answer for multi-user servers; for a single-workspace personal server it's complexity without benefit.Structured logs to stdout. One JSON line per HTTP request and per tool call (name, outcome, duration — never arguments, which can contain page content). Railway captures stdout;
/healthis excluded so the poller doesn't drown the signal.
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
MCP-native open-source Notion alternative: read & write pages, databases and kanban boards.
Nifty's MCP server — exposes tasks, projects, messages, and files as tools for AI agents.
Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer
Related MCP Servers
- AlicenseAqualityAmaintenanceMarkdown-first MCP server for Notion that provides 7 composite action-based tools consolidating 28+ REST API endpoints, enabling AI agents to efficiently manage pages, databases, blocks, and content with automatic pagination and bulk operations.11848 npm36Apache 2.0
- AlicenseNot gradedqualityCmaintenanceStateless MCP server exposing the full Notion API as 33 tools, enabling users to manage pages, databases, blocks, comments, users, and file uploads using their own integration token.Apache 2.0
- AlicenseNot gradedqualityDmaintenanceMCP server for the Notion API, enabling management of pages, blocks, databases, data sources, comments, and users through natural language.4 npm3MIT
- AlicenseNot gradedqualityDmaintenanceComprehensive MCP server for the Notion API. Provides 22 tools for full CRUD operations on pages, databases, blocks, users, and comments.10 npmMIT