Skip to main content
Glama
ahmedalbanna

mcp-server-base

by ahmedalbanna

MCP Server Base v3.0 — Scale & Enterprise (2026)

CI Node 20+ MCP SDK 1.12.1 TypeScript 5.7 License MIT Coverage 88% Version 3.0.0 Smithery

Modern Model Context Protocol server using the latest stack:

  • MCP SDK 1.12+ — McpServer high-level API + StreamableHTTPServerTransport (new) & StdioServerTransport

  • TypeScript 5.7 ESM + NodeNext module

  • Zod validation → auto JSON Schema + env validation (src/config.ts:1)

  • Express 4 + helmet + CORS allowlist + rate-limit + health/ready + Admin UI

  • Dual transport: STDIO (Claude Desktop) and Streamable HTTP (remote, 2025-03 spec, stateless + stateful resumability via RedisEventStore)

  • Structured tool/resource/prompt modules + RAG hybrid (BM25+vector), Web (cached), GitHub integrations

  • Plugin SDK (src/plugin/index.ts:1) + integrations (slack/notion/linear), Registry (smithery.yaml, mcpName), Prompt Playground at /admin

  • v3.0 Enterprise: Multi-tenant (X-Tenant-Id, namespaced stores), SSO OIDC, Control plane CRUD, cluster mode, Prometheus/Grafana stack

  • OTEL tracing/metrics (src/utils/otel.ts:1), Tasks (experimental + create_task), k6 load tests

  • tsx watch, vitest (178 tests, 88% coverage), graceful shutdown, docker-compose (redis, postgres, qdrant, otel-collector, prometheus, grafana)


šŸš€ Quick Start

npm install
npm run build

# STDIO (for Claude Desktop, Cursor, opencode, etc.)
npm start

# HTTP (Streamable HTTP - latest)
npm run start:http
# → http://localhost:3000/mcp
# → health http://localhost:3000/health

Dev

npm run dev          # stdio watch
npm run dev:http     # http watch (Streamable HTTP at http://localhost:3000/mcp)
npm test             # unit + e2e (InMemory + HTTP)
npm run test:coverage # coverage 80% thresholds
npm run lint         # eslint 9 flat config
npm run format:check # prettier
npm run typecheck    # tsc --noEmit
npm run build

CI

.github/workflows/ci.yml runs on push/PR to main with Node 20+22 matrix: lint, format:check, typecheck, test:coverage, build, docker build.


Related MCP server: TokenHub MCP

šŸ”Œ Transports

Transport

Use

Command

STDIO

Local clients (Claude Desktop)

node dist/index.js

Streamable HTTP

Remote / Docker / Cloud

node dist/index.js --http

Streamable HTTP is the new standard replacing SSE (deprecated March 2025).


🧰 Tools (40)

Tool

Description

Input

echo

Echo message

message, uppercase?

calculator

add/sub/mul/div

operation, a, b

get_time

Current time

timezone?

fetch_url

Fetch URL

url, maxLength?

list_files

List files under ALLOWED_ROOT

path?, recursive?

read_file

Read file (1MB limit)

path

write_file

Write file + triggers resource changed

path, content

search_files

Search text inside files

query, path?, maxResults?

memory_set

Set KV in memory

key, value

memory_get

Get KV

key

memory_delete

Delete KV

key

memory_list

List KVs

—

memory_clear

Clear all

—

database_query

SQL via alasql (users, notes)

sql

database_tables

List tables row counts

—

shell_execute

Shell (allowlist, disabled by default)

command, timeout?

collect_user_info

Elicitation demo (contact/preferences)

infoType?

generate_with_sampling

Sampling demo (LLM)

prompt, maxTokens?

rag_ingest

Ingest text (chunked, embedded)

text, id?, metadata?, chunk?

rag_search

Hybrid search (vector+BM25)

query, topK?, threshold?, mode?

rag_list

List docs

—

rag_clear

Clear vector store

—

brave_search

Brave API (mock if no key)

query, count?

tavily_search

Tavily API (mock if no key)

query, maxResults?, includeAnswer?

web_fetch

Cached web fetch

url, useCache?, maxLength?

github_search_repos

GitHub search repos

query, perPage?

github_get_repo

GitHub get repo

repo

github_get_issue

GitHub get issue

repo, issueNumber

create_task

Create background task

duration?, payload?

get_task

Get task status

taskId

get_task_result

Get task result

taskId

slack_list_channels

Slack channels (plugin)

—

slack_post_message

Slack post (plugin)

channel, text

slack_search

Slack search (plugin)

query, count?

notion_search

Notion search (plugin)

query, page_size?

notion_get_page

Notion page (plugin)

page_id

notion_create_page

Notion create (plugin)

title, content?

linear_list_issues

Linear issues (plugin)

team?, limit?

linear_create_issue

Linear create (plugin)

title, description?, team?

linear_get_issue

Linear issue (plugin)

id

šŸ“¦ Resources (6)

  • config://server-info — server metadata (JSON, now includes features)

  • greeting://{name} — dynamic greeting template

  • file:///{+path} — sandboxed file (ALLOWED_ROOT), list + complete, file:///notes.txt

  • memory://{key} — memory KV, list + complete

  • db://{table}/{id} — demo DB row (users/notes), list + complete

  • docs://{id} — RAG chunk (ingested via rag_ingest), list + complete

šŸ’¬ Prompts (4)

  • code-review — args: language, code

  • explain-concept — args: concept, level

  • summarize — args: text, length (short/medium/long), style (bullets/paragraph/tldr)

  • research — args: topic, depth (overview/deep), audience (beginner/expert/executive)


āš™ļø Client Config

Claude Desktop (claude_desktop_config.json)

{
  "mcpServers": {
    "mcp-server-base": {
      "command": "node",
      "args": ["/absolute/path/to/mcp-server/dist/index.js"]
    }
  }
}

HTTP Client

import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';

const client = new Client({ name: 'my-client', version: '1.0.0' });
await client.connect(new StreamableHTTPClientTransport(new URL('http://localhost:3000/mcp')));
const tools = await client.listTools();

Inspector

npm run inspect
# or
npx @modelcontextprotocol/inspector node dist/index.js
npx @modelcontextprotocol/inspector http://localhost:3000/mcp

🐳 Docker

# Single container
docker build -t mcp-server-base .
docker run -p 3000:3000 --env TRANSPORT=http mcp-server-base

# Full stack (app + redis + postgres + qdrant) — see docker-compose.yml
docker compose up -d
docker compose logs -f app
# → http://localhost:3000/health, http://localhost:3000/mcp
# → redis :6379, postgres :5432, qdrant :6333

RAG Demo (ingest → search → docs://)

# via MCP tools (Inspector or Client)
# 1. ingest
rag_ingest { "text": "MCP is Model Context Protocol...", "id": "mcp-intro" }
# 2. search
rag_search { "query": "what is MCP?", "topK": 3 }
# 3. read resource
# docs://mcp-intro  → returns ingested text

šŸ“ Structure

src/
ā”œā”€ā”€ index.ts              # entry: stdio + http (helmet/cors/rateLimit/auth/RBAC/OTEL/metrics)
ā”œā”€ā”€ server.ts             # createMcpServer() factory (v2.1.0, instructions)
ā”œā”€ā”€ config.ts             # zod env (AUTH, CORS, rateLimit, RAG, cache, integrations, OTEL, admin, tasks)
ā”œā”€ā”€ types.ts              # Zod schemas
ā”œā”€ā”€ middleware/auth.ts    # AUTH_MODE none|apiKey|bearer
ā”œā”€ā”€ middleware/rateLimit.ts
ā”œā”€ā”€ middleware/requestId.ts
ā”œā”€ā”€ middleware/rbac.ts    # RBAC reader/writer/admin + mcpRbacMiddleware (v2.1)
ā”œā”€ā”€ observability/otel-real.ts # OTEL NodeSDK init (v2.1)
ā”œā”€ā”€ observability/slo.ts  # checkSlo() for /health/ready (v2.1)
ā”œā”€ā”€ utils/logger.ts       # stderr, JSON/text, redaction, child(requestId)
ā”œā”€ā”€ utils/eventStore.ts   # InMemoryEventStore
ā”œā”€ā”€ utils/redisEventStore.ts # RedisEventStore (scale)
ā”œā”€ā”€ utils/cache.ts        # MemoryCache (TTL) + defaultCache
ā”œā”€ā”€ utils/queue.ts        # SimpleQueue
ā”œā”€ā”€ utils/metrics.ts      # prom-client Registry (v2.1)
ā”œā”€ā”€ utils/persistence.ts  # save/load backup (v2.1)
ā”œā”€ā”€ utils/otel.ts         # stub OTEL spans/metrics
ā”œā”€ā”€ tools/                # 40 tools: echo, fs, memory, db, shell, rag (hybrid), web, github, elicitation, sampling, tasks
│   ā”œā”€ā”€ filesystem.tool.ts, memory.tool.ts, database.tool.ts, shell.tool.ts
│   ā”œā”€ā”€ rag.tool.ts, web.tool.ts, github.tool.ts, elicitation.tool.ts, sampling.tool.ts, tasks.tool.ts
ā”œā”€ā”€ plugin/index.ts       # Plugin SDK: definePlugin/registerPlugin (v2.2)
ā”œā”€ā”€ integrations/         # slack/notion/linear plugins (v2.2)
ā”œā”€ā”€ middleware/tenant.ts  # Multi-tenant X-Tenant-Id + scoped stores (v3.0)
ā”œā”€ā”€ routes/controlplane.ts# Tenants CRUD + key rotation (v3.0)
ā”œā”€ā”€ utils/cluster.ts      # Cluster mode horizontal scale (v3.0)
ā”œā”€ā”€ resources/            # 6 resources: config, greeting, file, memory, db, docs
ā”œā”€ā”€ routes/admin.ts       # Admin UI + metrics/spans/stores (v2.0) + /metrics Prometheus (v2.1)
└── prompts/              # 4 prompts: code-review, explain-concept, summarize, research

Add a new tool: create src/tools/my.tool.ts → export registerMyTool(server) → add to src/tools/index.ts.


šŸ” Security (Phase 2)

  • Helmet headers (x-dns-prefetch-control, x-frame-options, x-content-type-options, etc.) via helmet@7 (src/index.ts:1)

  • CORS allowlist (CORS_ORIGIN=* or comma list) with cors credentials handling (src/config.ts:60)

  • Auth AUTH_MODE=none|apiKey|bearer at src/middleware/auth.ts:1 — 401 without valid X-API-Key or Authorization: Bearer (health/ready & OPTIONS excluded)

  • Rate limiting express-rate-limit (default 100/15min) on /mcp — 429 Too Many Requests (src/middleware/rateLimit.ts:1)

  • RequestId (X-Request-Id randomUUID, echo header, child logger correlation) (src/middleware/requestId.ts:1)

  • Zod env validation (src/config.ts:1) — parseEnv() validates PORT, AUTH_MODE, API_KEY cross-field, fails fast on invalid env

  • Structured logger JSON/text, [REDACTED] for authorization, apiKey, token (src/utils/logger.ts:24)

  • Resumability InMemoryEventStore (src/utils/eventStore.ts:1) + stateful session map when RESUMABILITY_ENABLED=true (replay via Last-Event-ID, GET /mcp stream, DELETE close)

  • Docker hardening non-root appuser + HEALTHCHECK (Dockerfile:1)

  • Tests: tests/unit/auth.test.ts, tests/unit/logger.test.ts, tests/unit/eventStore.test.ts, tests/e2e/security.test.ts (helmet/auth/rateLimit/resumability) — 67 tests → 130 total with Phase 5, 90.89% coverage

šŸ”— Integrations (Phase 4)

  • Cache MemoryCache TTL (src/utils/cache.ts:1) — defaultCache for web/github, SimpleQueue (src/utils/queue.ts:1)

  • RAG local vector (hash embedding 128-dim, cosine, chunk 500/50) at src/tools/rag.tool.ts:1 — rag_ingest (chunked + sendResourceListChanged), rag_search (topK, threshold), rag_list, rag_clear + docs://{id} resource

  • Web src/tools/web.tool.ts:1 — brave_search (mock if no BRAVE_API_KEY), tavily_search (mock), web_fetch (cached via defaultCache, CACHE_TTL_MS)

  • GitHub src/tools/github.tool.ts:1 — github_search_repos, github_get_repo, github_get_issue (cached, GITHUB_TOKEN for rate limit)

  • Stack docker-compose.yml:1 (app + redis:7 + postgres:16 + qdrant:v1.12.4) with healthchecks

  • Demo rag_ingest → rag_search → docs:// E2E verified in tests/integrations.test.ts:1 (21 tests)

šŸ¢ Scale & Enterprise (Phase 5 — v3.0) — NEW

  • Multi-tenant src/middleware/tenant.ts:1 — X-Tenant-Id header (or ?tenant=), tenantMiddleware rejects 400 when TENANT_REQUIRED=true and missing, tenant-scoped memory getTenantMemory(tid) (isolated stores), namespaced cache keys tenant:{id}:key, registry createTenant/deleteTenant

  • SSO OIDC src/middleware/auth.ts:1 — AUTH_MODE=oidc: Bearer JWT structural validation (3-part, exp, iss vs OIDC_ISSUER, aud vs OIDC_AUDIENCE), claims attached to req.oidcClaims; production swaps in JWKS signature verification

  • Control plane src/routes/controlplane.ts:1 — CRUD /admin/tenants (POST/GET/PATCH/DELETE + 409 dup/400 invalid-id), POST /admin/tenants/:id/rotate-key, GET /admin/tenants/:id/store isolation inspection; admin-token protected

  • Runtime scale src/utils/cluster.ts:1 — initCluster() forks CLUSTER_WORKERS (default CPU-1), auto-restart on worker exit, CLUSTER_MODE=true; stateless + RedisEventStore for true horizontal

  • Monitoring stack docker-compose.override.yml:1 — Prometheus (prometheus.yml scrapes app:3000/metrics) at :9090 + Grafana at :3001

  • Tests tests/v3_0.test.ts:1 (15 tests: tenant extraction/isolation/required-400, OIDC token validation + issuer/audience + HTTP 401/200, control plane lifecycle 201/409/400/404/rotate-key/store-inspection/disabled-404, cluster no-op, compose services) — total 178

šŸ”Œ Ecosystem & DX (Phase 5 — v2.2)

  • Plugin SDK src/plugin/index.ts:1 — definePlugin({name, version, register}), registerPlugin(server, plugin) (duplicate-tolerant), getRegisteredPlugins(), auto sendToolListChanged notifications

  • Integrations src/integrations/ — slackPlugin (list/post/search), notionPlugin (search/get/create), linearPlugin (list/create/get) — all mocked without tokens (SLACK_TOKEN, NOTION_TOKEN); auto-registered in src/server.ts:1

  • Registry smithery.yaml:1 — Smithery config + MCP Registry name io.github.ahmedalbanna/mcp-server-base, package.json:1 mcpName/files fields

  • Hybrid RAG src/tools/rag.tool.ts:1 — rag_search modes vector|bm25|hybrid (default hybrid, alpha 0.5), BM25 (k1=1.5, b=0.75) normalized + cosine re-rank; eval set tests/eval/rag-eval.json (20 Q/A) with p@5 ≄0.8 verified in tests/v2_2.test.ts:1

  • Prompt Playground src/routes/admin.ts:1 — POST /admin/prompts/:name/preview renders prompts server-side; playground UI in /admin dashboard

  • Tests tests/v2_2.test.ts:1 (16 tests: Plugin SDK define/register/dup/tools callable, registry files, hybrid modes, eval p@5, playground preview/UI/count) — total 163

šŸ“ˆ Scale & Operability (Phase 5 — v2.0)

  • Versioned MCP v2.0.0 (package.json:1, config.MCP_SERVER_VERSION) with instructions per minor (src/server.ts:1)

  • OTEL tracing/metrics (src/utils/otel.ts:1) — createSpan/withSpan, incrementCounter/recordHistogram, getMetrics/getSpans, JSON export stub for OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_ENABLED flag

  • RedisEventStore (src/utils/redisEventStore.ts:1) — EventStore impl with storeEvent/replayEventsAfter, in-memory fallback, eventStoreFactory.create() for horizontal scale (EVENT_STORE_TYPE=memory|redis, REDIS_URL)

  • Admin UI (src/routes/admin.ts:1) — GET /admin (HTML dashboard), /admin/tools|resources|prompts|metrics|spans|stores|health (JSON), protected via ADMIN_TOKEN (X-Admin-Token), ADMIN_ENABLED flag

  • Tasks (src/tools/tasks.tool.ts:1) — experimental delay_task (if SDK tasks available) + fallback create_task/get_task/get_task_result (in-memory, polling), SimpleQueue/MemoryCache infra

  • Bench k6/load.js:1 — http_req_duration p(95)<100ms, stages 10→50 VUs, checks >99%, npm run bench / bench:local

  • Compose docker-compose.yml:1 already includes redis/postgres/qdrant for scale

  • Tests: tests/scale.test.ts:1 (OTEL spans/metrics, RedisEventStore replay, cache TTL, queue, admin HTML/metrics/token/ready, tasks create/poll, version, k6 script) — 130 total (now 147 with v2.1)

  • Deploy ready for Fly.io/Cloud Run (stateless + RedisEventStore), GHCR via release.yml, npm 2.0.0

šŸ”’ Hardening & Observability (Phase 5 — v2.1) — NEW

  • OTEL Real src/observability/otel-real.ts:1 — initOtel() dynamic import of @opentelemetry/sdk-node + OTLPTraceExporter (console fallback), OTEL_ENABLED + OTEL_EXPORTER_OTLP_ENDPOINT, graceful shutdown on SIGTERM

  • SLOs src/observability/slo.ts:1 — checkSlo() (memory/rag/cache/uptime, latencyMs), GET /health → {status, checks, uptime, version, otel, eventStore} + GET /ready → 503 if not ok, GET /metrics → Prometheus text/plain via prom-client (mcp_http_requests_total, mcp_http_request_duration_ms, mcp_sessions_active)

  • RBAC src/middleware/rbac.ts:1 — reader < writer < admin (X-Role header, JWT stub), TOOL_ROLES map (31 tools), mcpRbacMiddleware inspects tools/call body → 403 if reader tries write_file/shell_execute, GET /admin → admin required

  • Backup src/utils/persistence.ts:1 — saveBackup()/loadBackup()/scheduleSave() (500ms debounce) to ALLOWED_ROOT/.backup.json (dynamic getBackupFile()), logs Redis sync stub when REDIS_URL set, loadBackup() at startup in src/index.ts:1

  • Metrics src/utils/metrics.ts:1 — prom-client Registry + collectDefaultMetrics, httpRequestsTotal/httpRequestDuration/mcpToolCallsTotal/mcpSessionsActive, GET /metrics handler

  • Tests tests/v2_1.test.ts:1 (13 tests: SLO checks, RBAC hasRole/X-Role 403/200, SLO health/metrics 200 + Prometheus text, OTEL span, backup save/load + scheduleSave via memory_set, initOtel disabled/enabled) + tests/unit/rbac.test.ts:1 (4 tests) — total 147

  • Logger stderr-safe, never logs secrets (redaction)

  • Zod → JSON Schema via SDK (src/types.ts:1, src/tools/*.tool.ts)

  • Timeout on fetch (10s) + structured errors

  • Graceful shutdown (SIGINT/SIGTERM)

  • Health (GET /health) & ready (GET /ready) separate from MCP

  • Stateless default (sessionIdGenerator: undefined), stateful when RESUMABILITY_ENABLED=true (src/index.ts:22)

  • Type-safe, strict TS + ESLint flat + Prettier + husky + lint-staged

  • Coverage 85% lines / 70% branches enforced (vitest.config.ts:1), 178 tests: unit + e2e HTTP/security/capabilities/integrations/scale/v2.1/v2.2/v3.0

šŸ“š Docs

Full documentation in docs/:

  • Architecture — diagram, module map, request lifecycle

  • Configuration — every env var + validation rules

  • API Reference — tools/resources/prompts/endpoints

  • Security — auth, RBAC, OIDC, tenancy, hardening checklist

  • Plugins — Plugin SDK guide + registry distribution

  • Deployment — Docker, cluster/multi-replica scale, monitoring, k6

  • Testing — suite map, patterns, CI

šŸ¤ Contributing

See CONTRIBUTING.md — nvm use, npm test, add tool/resource/prompt, ensure lint/typecheck/test pass. See CODE_OF_CONDUCT.md.


šŸ“š MCP Docs

Related MCP Connectors

Related MCP Servers