An MCP server built around Gemini-powered delegation — hand Claude an open-ended, multi-step investigation instead of chaining 5-10 manual tool calls — plus direct tool access to GitHub, Cloudflare, Notion, Mem0, Context7, Jules, and the web. Reflexive by construction: a connected agent has write access to this very repo, so it can read its own source, diagnose a gap, and patch it through the same connection.
madmcp's core idea is delegation: instead of an agent making 5-10
manual tool calls to run an investigation itself, it can hand an open-ended,
read-only investigation (or a single page + question) to Gemini, which runs
its own tool-use loop server-side and returns one synthesized answer. This
is split across two tools along a security boundary: delegate_agent
covers GitHub, Cloudflare, and Notion (no web access), while
delegate_research covers the live web (a precision single-page mode, or
a single-shot wide research mode via Exa's /answer endpoint) with no
access to those internal systems. Both are described first under Connectors & tools below.
On top of that, the server also gives an AI agent direct tool-level access to real infrastructure — GitHub, Cloudflare, Notion, Mem0, Context7, and arbitrary web pages — so agent workflows can read and write directly instead of relying on manual copy/paste between tabs.
DEFAULT_OWNER defaults to your forked repo, and the GitHub connector exposes
full read/write/PR tooling (read_file, edit_file,
pr_write with create / merge, etc.) — not a read-only subset.
That combination means an agent connected to this server has direct write
access to this very repo: it can read its own source, diagnose a bug or a
gap in the docs, and commit the fix — or open a PR against itself — through
the same connection it's already using, with no separate deploy step or
out-of-band access required. This isn't a hypothetical: this README, and the
portfolio page describing this project, have both been edited exactly this
way. Worth being precise about what this is not: the server doesn't
modify itself unprompted on some schedule — every change still starts with
an agent invoked by a person. What's notable is that there's no separate
admin path or special-cased self-access; a connected agent uses the exact
same edit_file/pr_write tools on this repo as on any
other repo it has a token for.
An illustrative six-step walkthrough of a mock incident response — finding a
slow endpoint, checking latency metrics, shipping and reviewing a fix, and
logging it to memory — touching all five connectors in one flow (including a
deeper look at Mem0's semantic dedup and relation-graph tools), with a synced
status panel and live call/latency stats alongside the trace. Sample data
throughout, not a live call. (If GitHub Pages isn't enabled for this repo
yet, open demo.html directly.)
Need connector tokens first? → Get API keys — one-click links to each provider's key-creation page, or use the env bundler below to collect them all and copy out a single .env block:
Get tokens, deploy, connect to Claude — in that order.
1. Deploy it — pick a host. Both buttons above deploy this repo as-is, no Dockerfile or CLI needed, but they're not identical:
-
Render reads
render.yamland generatesMCP_SHARED_KEYfor you automatically — one less thing to get wrong. Trade-off: the free plan spins down after 15 minutes idle, so the first request after a quiet period is slow (~30s cold start). Fine for testing; upgrade to a paid plan if you want Claude's calls to stay consistently fast. -
Vercel deploys with zero config (this repo's
server.jsalready matches what Vercel expects) and its Fluid compute avoids Render's cold-start spin-down. Trade-off: you have to generateMCP_SHARED_KEYyourself — any long random string, e.g. via a password generator oropenssl rand -hex 32— and paste it into the form; Vercel's button can't auto-fill it the way Render's blueprint does. -
Manufact Cloud is purpose-built for hosting MCP servers (this one included — the default IP allowlist already carries a comment about Manufact's own deploy-time health check). It doesn't have a one-click button with a public URL scheme the way Render/Vercel do; instead sign in, New Server → Import from GitHub → pick this repo, add your env vars on the Configure Deployment screen (or paste a
.env), then Deploy. Node.js is auto-detected frompackage.json, no Dockerfile needed. Free tier scales to zero like Render's free tier (cold starts); paid plans offer "Prevent Scale to Zero." Manufact's gateway URL pattern ishttps://<slug>.run.mcp-use.com/mcp— the/mcp/<key>path-auth variant this repo uses does survive that gateway routing (confirmed: it's the exact pattern in use), so the samehttps://<slug>.run.mcp-use.com/mcp/<your MCP_SHARED_KEY>URL from step 5 below works here too.
Prefer another host, or running it yourself? It's a plain Node/Express app —
npm install && npm start, listens on $PORT (default 8080) — so any
Node-friendly host (Railway, Fly.io, etc.) or your own server works too,
just without any platform's auto-fill.
A note on IP allowlisting across hosts: the allowlist trusts one
reverse-proxy hop by default (TRUST_PROXY_HOPS, default 1), which
matches Render and most single-CDN-hop platforms. If Claude's calls get
unexpectedly 403'd on a different platform, that hop count is the first
thing to check — adjust TRUST_PROXY_HOPS, or temporarily set
IP_ALLOWLIST_ENABLED=false to confirm that's the cause before tightening
it back up.
2. Collect tokens for the connectors you want. Each is independent — skip any you don't need, its tools just fail at call time instead of blocking the rest.
| Connector | Where to get it |
|---|---|
| GitHub | github.com/settings/tokens → fine-grained PAT, scoped to specific repos |
| Notion | notion.so/my-integrations → create an integration, then share the relevant pages/databases with it |
| Mem0 | app.mem0.ai → API keys |
| Cloudflare | dash.cloudflare.com → My Profile → API Tokens, plus your Account ID from the dashboard sidebar |
| Jules | jules.google.com/settings#api → API key (max 3 per account, no unauthenticated tier) |
3. Set two security env vars — don't skip these.
MCP_SHARED_KEY— any long random string. Unset =/mcpis open to anyone who has your server's URL, tokens and all.IP_ALLOWLIST_ENABLED— on by default, pre-set to Claude's published connector IP range, so leave it alone if you're only ever connecting from Claude.ai. Set it tofalsetemporarily to test with curl/Postman from your own machine first.
4. Deploy, then verify. GET /health returns {"status":"ok"}, no auth
needed. GET / (needs your x-manufact-key header — this endpoint doesn't
support the path-key variant) reports which connectors are configured, so
you can confirm your tokens landed.
5. Add it to Claude. Settings → Connectors → Add custom connector, using:
https://<your-host>/mcp/<your MCP_SHARED_KEY>
(Path-based, since Claude.ai's connector UI doesn't currently support header-based auth for MCP servers.)
See Configuration below for the full variable reference.
The two delegation tools split along a security boundary: delegate_agent
has no web access, delegate_research has no access to GitHub/Notion/
Cloudflare. This means a malicious page or search result encountered
mid-research can influence at most that run's own answer — it has no
internal-system data to exfiltrate, because that loop never has access to
any in the first place.
delegate_agent — hand an open-ended, multi-step, read-only investigation
(e.g. "why is CI failing on PR #42", "summarize what changed in this repo over
the last week") to Gemini (default), GLM, or Groq instead of making 5-10
separate manual tool calls. The model runs its own loop server-side across
GitHub, Cloudflare, and Notion (bounded by max_steps, default 6, hard cap
20) and returns one synthesized answer. Falls through an ordered model
cascade (GEMINI_MODEL → GEMINI_FALLBACK_MODELS, GLM_MODEL →
GLM_FALLBACK_MODELS across every key in OPENROUTER_API_KEYS, or
GROQ_MODEL → GROQ_FALLBACK_MODELS across every key in GROQ_API_KEYS)
on rate limits, with Redis-backed per-model cooldown so already-limited
models are skipped rather than retried. An explicit
provider: "gemini" | "glm" | "groq" arg picks which one backs a given
call (default: DEFAULT_LLM_PROVIDER, itself defaulting to "gemini") —
all three are interchangeable in capability, not just cost/speed; try
"glm" or "groq" if Gemini's output has been unreliable for a given
task. GLM (via OpenRouter) is currently non-functional on a zero-credit
account — see docs/API_KEYS.md — so Groq is the practical free-tier
alternative to Gemini for now (request/token-rate-limited rather than
credit-balance-gated, no card required). Ignored on a resume (the provider
that started the run is always reused, so a checkpointed conversation
can't be corrupted by resuming it on a different provider's wire format).
delegate_research — web research, in one of two mutually-exclusive modes
selected by which args are passed:
- Precision mode (
url+question): fetch a single URL and get back Gemini's answer to a specific question about its content, without returning the raw page. Use this instead ofweb_fetchwhen you need a distilled answer rather than exact wording to copy. - Wide mode (
task): a single-shot call to Exa's/answerendpoint, which does its own web search + synthesis server-side and returns one answer with sources. No multi-step loop, no effect frommax_steps, and nothing to resume viaresume_run_id(that Gemini-loop architecture was retired 2026-07-27).
Progress on delegate_agent runs is checkpointed to Redis after every
completed step. If the underlying Gemini API call fails partway through
(429/503/network blip), the response includes a resume_run_id and
everything gathered so far instead of losing the run outright — pass that
id back on a follow-up call to continue from the last completed step
(checkpoint TTL: 1 hour) rather than re-running, and re-paying for, steps
already done. Wide-mode delegate_research is a single Exa call with no
steps to checkpoint — a failure just returns an error, and resume_run_id
is accepted only for parameter compatibility with delegate_agent.
Both tools can optionally log their task/question, step-by-step tool calls,
and final answer to a Notion page under a fixed Gemini root page
(log_to_notion, default false).
File/repo ops: read_file (accepts optional char_offset/char_limit to page through large files), list_directory,
get_file_tree, create_repo_file, edit_file, overwrite_files, rename_file,
delete_file
Repo inspection (read-only): repo_inspect — one tool, selected by action: at_commit (file contents at a commit SHA), diff, branch_protection, list_branches, list_commits, get_commit. Consolidated from the former get_file_at_commit, diff_files, get_branch_protection, list_branches, list_commits, and get_commit tools.
Code search: search_code — standalone; requires ref and a repo:owner/name qualifier in query. The rest of the query is matched as one literal string (no OR; filename:/extension: qualifiers are stripped).
Branch creation (mutating): create_branch — standalone; requires owner, repo and the new branch name, with optional from_branch.
Issues: issue_manage (action: get | list | create | update | comment | search), replacing get_issue, list_issues, create_issue, update_issue, add_issue_comment, search_issues. search is cross-repo and ignores owner/repo (scope it with qualifiers in query); comment works on PRs too.
Pull request writes (mutating): pr_write (action: create | update | merge | review | request_reviewers | remove_reviewers | inline_comment), replacing create_pull_request, update_pull_request, merge_pull_request, review_pull_request, request_reviewers, remove_requested_reviewers, add_review_comment. merge is irreversible via this tool.
Pull request reads: pr_read (action: list | get | activity | mergeability), read-only, replacing get_pull_requests, get_pr_activity, get_pr_mergeability. mergeability polls server-side (up to 4 tries) since GitHub computes it async.
Releases & tags: release_manage (action: list | create | list_tags), replacing list_releases, create_release, list_tags
Repo metadata (read-only): repo_metadata (action: list | get | contributors | topics), replacing list_repos, get_repo, list_contributors, get_repo_topics
Repo lifecycle (mutating): repo_lifecycle (action: create | fork | sync_fork | delete | set_topics), replacing create_repo, fork_repo, sync_fork, delete_repo and the topics write path. delete is permanent and requires confirm: true.
CI / Actions: ci_manage (action: list | run_logs | job_logs | trigger | rerun | cancel | checks | status), replacing list_workflow_runs, get_workflow_run_logs, get_job_logs, trigger_workflow, rerun_workflow, cancel_workflow_run, get_check_runs, get_combined_status. trigger, rerun and cancel mutate; the rest are read-only. list, checks and status each carry their own sleep-and-recheck guidance while runs/checks are pending.
Notifications: list_notifications
Codespaces: codespace_manage (action: list | get | machines | create | start | stop | delete), replacing list_codespaces, get_codespace, list_codespace_machines, create_codespace, start_codespace, stop_codespace, delete_codespace. create, start, stop and delete mutate; delete is permanent. Requires the codespace PAT scope on GITHUB_TOKEN — see API_KEYS.md. Runtime requirement: exec_in_codespace (separate, gated by CODE_EXEC_ENABLED) requires the GitHub CLI (gh) to be installed and authenticated on the server running this MCP instance.
D1: cf_d1_read (action: get | list), cf_d1_manage (action: create), cf_d1_query
KV: cf_kv_read (action: get | list), cf_kv_manage (action: create | update)
R2: cf_r2_read (action: get | list), cf_r2_manage (action: create)
Hyperdrive: cf_hyperdrive_read (action: get | list), cf_hyperdrive_manage (action: update)
Workers: cf_workers_read (action: list | get | code)
Observability: cf_workers_observability (action: query | keys | values | compare)
Delete: cf_delete (resource: d1 | kv | r2 | hyperdrive, id, confirm), replacing cf_d1_database_delete, cf_kv_namespace_delete, cf_r2_bucket_delete, cf_hyperdrive_config_delete. Irreversible; refuses unless confirm: true. For r2, id is the bucket name.
notion_find, notion_read, notion_create, notion_update, notion_sync_content,
notion_index_entries_add_batch — consolidated from a former 12-tool surface
(notion_search/notion_list → notion_find; notion_get_page/notion_get_page_history/
notion_get_database/notion_query_database → notion_read; notion_create_page/
notion_create_pages_batch/notion_create_database → notion_create; notion_update_page/
notion_update_pages_batch/notion_update_database → notion_update), each now taking a
type/mode arg to select page vs. database behavior. notion_index_entries_add_batch is a
separate backfill/repair tool for the entity_id → page_id dedup index, not part of the
consolidation.
notion_chart (action: create | update | get | list | delete | create_dashboard |
create_from_data) — creates and manages native Notion chart views (column, bar, line, donut,
number) through the Views API. Without page_id/dashboard_id a chart becomes a view tab on the
database; with page_id (+ optional after_block_id) it is placed inline on that page; with
dashboard_id it becomes a widget in that dashboard. Properties are given by name or id.
update keeps every existing chart setting you don't change (Notion replaces the whole
configuration on PATCH, so the tool re-sends it). dry_run: true returns the exact request body
without writing (it still does read-only schema/view lookups). Examples:
{action: "create", database_id, name: "Tasks by status", chart_type: "column", x: "Status"}.
Dashboards: {action: "create_dashboard", database_id, name} makes an empty dashboard view, then
add charts with {action: "create", database_id, dashboard_id, placement: {type: "new_row" | "existing_row", row_index}, ...} (default placement: a new row at the end; existing_row puts the
widget side by side with that row). Notion has no API to move widgets or change the layout after
creation. Dashboards need a Business or Enterprise Notion plan.
Ad-hoc data: {action: "create_from_data", parent_page_id, database_title, columns: [{name, type}], rows: [{<column>: value}], chart_type, x, ...} creates a new database under that page, inserts
the rows, then adds the chart (name defaults to database_title; page_id/dashboard_id
placement also work). Column types: title (exactly one), text, number, select, date
(ISO 8601), checkbox. Max 50 rows and 25 columns per call. The data and the chart settings are
validated before anything is written; if a later step fails, the error says what was already
created (the database is left in place).
Requirements: a paid Notion plan (free workspaces get one chart) and an integration with the
capability to create views. Views calls send Notion-Version: 2026-03-11 per request only; the
global NOTION_VERSION stays 2022-06-28 because the entity index relies on
/databases/{id}/query. Not supported yet: raw "results" mode.
mem0_write (action: add | add_batch | update — replaces the former mem0_add,
mem0_add_batch and mem0_update), mem0_inspect (action: get | history | relations —
replaces the former mem0_get, mem0_get_history and mem0_get_relations), mem0_find
(action: list | search — replaces the former mem0_list and mem0_search), mem0_delete
(action: one | batch | all — replaces the former mem0_delete, mem0_delete_batch and
mem0_delete_all)
search_library, get_library_docs — resolve a library/framework name to a Context7 ID, then fetch
up-to-date, version-specific docs and code examples for it. Works without an API key at low rate
limits; CONTEXT7_API_KEY is optional.
sync_mem0_to_notion — one-way sync from Mem0 into a Notion "Memory Index": creates/updates a
Notion page per Mem0 memory, archives pages for superseded or hard-deleted memories, and leaves any
manual edits on those pages untouched.
jules_find (sources | sessions), jules_inspect (session | activities), jules_write (create | message) — hand off a coding task to Google's Jules agent, fire-and-forget: it works in its own sandboxed VM against a connected GitHub repo and (by default, via automation_mode: AUTO_CREATE_PR) opens a PR when done, with plans auto-approved. Distinct from delegate_agent/delegate_designer in this repo: those are synchronous read-only (or frontend-fenced) loops returning one answer per call; a Jules session is asynchronous and can write arbitrary code across a whole repo over several minutes, independent of this server's request lifecycle — create it with jules_write, then poll jules_inspect later. Alpha API (Google's own designation), no unauthenticated tier.
web_fetch — fetch a public URL and return text/JSON/stripped HTML
All tokens are optional independently — a connector's tools fail at call time (not startup) if its token is missing.
| Variable | Required for |
|---|---|
GITHUB_TOKEN |
GitHub tools |
NOTION_TOKEN |
Notion tools |
MEM0_API_KEY |
Mem0 tools (MEM0_USER_ID optional, defaults to default) |
CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID |
Cloudflare tools |
CONTEXT7_API_KEY |
Context7 tools (optional — works unauthenticated at low rate limits) |
JULES_API_KEY |
Jules tools (jules_*) — required, no unauthenticated tier |
GEMINI_API_KEYS |
Gemini tools (delegate_agent, delegate_research) — comma-separated, multi-key cascade; required (or legacy singular GEMINI_API_KEY), throws if unset |
GEMINI_MODEL |
Primary Gemini model for delegation (default gemini-3.8-flash) |
GEMINI_FALLBACK_MODELS |
Comma-separated fallback model list used on 429s/503s/network errors, and on 404s when a model ID has been retired (default gemini-3.7-flash,gemini-3.6-flash,gemini-3.5-flash,gemini-3.5-flash-lite) |
GEMINI_NOTION_ROOT_PAGE_ID |
Notion page under which Gemini tool outputs are logged (has a working default) |
OPENROUTER_API_KEYS |
Comma-separated OpenRouter API key(s) — required for delegate_agent's provider: "glm" mode, unused otherwise |
GLM_MODEL |
Primary GLM model (via OpenRouter) for provider: "glm" delegation (default z-ai/glm-4.5-air:free) |
GLM_FALLBACK_MODELS |
Comma-separated fallback model list used on 429s, cascaded per OPENROUTER_API_KEYS key (default: none configured) |
GLM_REQUEST_TIMEOUT_MS |
Defensive ceiling on a single GLM/OpenRouter call (default 55000) |
GROQ_API_KEYS |
Comma-separated Groq API key(s) — required for delegate_agent's provider: "groq" mode, unused otherwise; free-tier is request/token-rate-limited, not credit-balance-gated like OpenRouter/GLM |
GROQ_MODEL |
Primary Groq model for provider: "groq" delegation (default openai/gpt-oss-120b, a production model) |
GROQ_FALLBACK_MODELS |
Comma-separated fallback model list used on 429s, cascaded per GROQ_API_KEYS key (default qwen/qwen3.6-27b — stronger on benchmarks but a Groq preview model, kept as fallback rather than primary for availability reasons) |
GROQ_REQUEST_TIMEOUT_MS |
Defensive ceiling on a single Groq call (default 55000) |
DEFAULT_LLM_PROVIDER |
Which provider delegate_agent uses when a call omits provider (default gemini) |
UPSTASH_REDIS_REST_URL + UPSTASH_REDIS_REST_TOKEN (or KV_REST_API_URL + KV_REST_API_TOKEN) |
Optional — persists per-model rate-limit cooldowns and delegate_agent resume checkpoints across invocations; fails open if neither pair is set. Either naming works — the raw Upstash Marketplace integration names them UPSTASH_REDIS_REST_*, Vercel's own "KV" product (also Upstash-backed) names them KV_REST_API_*. |
DEFAULT_OWNER |
Default GitHub owner when omitted from a call (defaults to allocsys) |
GITHUB_MIN_REQUEST_INTERVAL_MS |
Minimum spacing between outgoing GitHub REST requests, to avoid secondary rate limits (default 300) |
GITHUB_MAX_RETRIES |
Max retries on GitHub secondary-rate-limit/429 responses (default 3) |
GITHUB_RETRY_BASE_MS |
Fallback backoff base when GitHub omits Retry-After (default 1500, doubles per retry) |
NOTION_INDEX_DATABASE_ID |
Database used for entity_id → page_id dedup lookups. Set this yourself if you're self-hosting — the default points to a database in the original deployer's own Notion workspace, which your integration won't have access to. Create a database in your workspace, share it with your integration (notion.so/my-integrations), and use its ID (the 32-char segment in its URL). |
NOTION_SYNC_PARENT_PAGE_ID |
Parent page for pages created by sync_mem0_to_notion. Set this yourself if you're self-hosting, same reason as above — the default points to a page in the original deployer's workspace. Share a page in your own workspace with your integration and use its page ID. |
MCP_SHARED_KEY |
Shared-secret auth for /mcp. Unset = endpoint is open to anyone with the URL — set this in any real deployment. |
IP_ALLOWLIST_ENABLED |
Set false to disable the IP allowlist (default: enabled) |
ALLOWED_IP_RANGES |
Comma-separated CIDR ranges allowed to call /mcp (defaults to Anthropic's published connector range) |
TRUST_PROXY_HOPS |
Number of reverse-proxy hops to trust for client-IP detection (default 1, matches Render). Adjust if deploying behind a different proxy chain. |
- Requests to
/mcpare restricted by IP allowlist and rate-limited (30 requests/min). - Scope
GITHUB_TOKENas narrowly as possible (ideally fine-grained, limited to specific repos). This server can create/delete repos and files, merge PRs, and delete Cloudflare resources — treat every configured token as live write access. GET /reports which connectors are configured;GET /healthstays open and info-free for uptime checks.- Rotate any token if you ever suspect it's been exposed.
Licensed under AGPL-3.0 with the Commons Clause. In plain terms:
- You can use, run, modify, and self-host this server, including for internal business use — for free.
- If you modify it and let others interact with your version over a network (e.g. host it as a service), you must publish the source of your changes (AGPL-3.0's network-copyleft requirement).
- You may not sell it — the Commons Clause blocks offering a paid product or service whose value comes substantially from this software's functionality, including paid hosting of it, without a separate agreement with the licensor.
See LICENSE for the full, binding text. This summary is for
convenience only and isn't a substitute for reading it (and isn't legal
advice).
export GITHUB_TOKEN=ghp_yourtokenhere
npm install
npm startServer listens on PORT (default 8080). MCP endpoint: POST /mcp.
Health check: GET /health.
npm install
npm testRuns the unit tests (vitest) covering the IP-allowlist/auth helpers
(connectors/security.js), the GitHub client's retry/backoff logic
(connectors/github/client.js), and the Jules client/tools
(connectors/jules/). CI (.github/workflows/ci.yml) runs these
same tests on every push and PR, plus a syntax check and a boot/GET /health
smoke test, as a merge gate — it does not deploy; Render and Vercel already
auto-deploy on push via their own GitHub integrations.