CT

camerhann/terramcp

开发工具
0 stars 0 forks 质量 57 趋势 20

Ranked directory + MCP router for water, environmental, terrain & civil engineering data, MCPs and skills

概览

Curated, ranked MCP servers, agent skills, plugins and open data for water, environment, terrain/GIS and civil engineering consultants worldwide. Includes a stateless, read-only remote MCP endpoint and a sponsored consultancy listing. TerraMCP is a Wrench AI service. 35 real sources + 2 sponsored coming-soon slots (labelled Sponsored). Sponsorship never changes scores or organic ranking; placeholders are excluded from organic results but may appear in disclosed sponsored placements. v1 has MCP, plugin, API and dataset seed entries; the model/filters also support reviewed skills and software. Research, business plan, scoring formulas and hosting limits: PLAN.md. Node and npm. No database, account, API key or paid service is required. Open http://localhost:3000. Without a DB binding, pages, JSON API and MCP read the committed data/listings.json. Submissions return HTTP 503 with a clear message and do not claim to save. data/seed.

README

TerraMCP

Curated, ranked MCP servers, agent skills, plugins and open data for water, environment, terrain/GIS and civil engineering consultants worldwide. Includes a stateless, read-only remote MCP endpoint and a sponsored consultancy listing. TerraMCP is a Wrench AI service.

35 real sources + 2 sponsored coming-soon slots (labelled Sponsored). Sponsorship never changes scores or organic ranking; placeholders are excluded from organic results but may appear in disclosed sponsored placements. v1 has MCP, plugin, API and dataset seed entries; the model/filters also support reviewed skills and software. Research, business plan, scoring formulas and hosting limits: PLAN.md.

Run locally

Node 22+ and npm. No database, account, API key or paid service is required.

npm ci
npm run dev

Open http://localhost:3000. Without a DB binding, pages, JSON API and MCP read the committed data/listings.json. Submissions return HTTP 503 with a clear message and do not claim to save. data/seed.json is the curated source of truth; scoring/enrichment regenerate both JSON files together.

npm run build
npm run lint
npx tsc --noEmit
npm test
npm run start
# In another terminal:
npm run test:mcp

test:mcp connects through the official SDK, lists tools, calls all five, checks sorting, provenance, placeholders, invalid arguments, missing slugs and concurrent stateless requests. Override MCP_TEST_URL for another running instance. The integration check also verifies router tools reject unauthenticated requests.

Environment

Variable Required? Purpose
CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID Deployment only Cloudflare API token and target account ID; shell or Actions secrets only.
GH_TOKEN or GITHUB_TOKEN Discovery/enrichment only GitHub CLI auth. Workflow uses repository GITHUB_TOKEN; no extra PAT required.
MCP_ALLOWED_ORIGINS Optional Comma-separated exact browser origins allowed at /mcp in addition to its own origin, e.g. https://client.example. No wildcards.
MCP_TEST_URL Optional Test URL; default http://localhost:3000/mcp.
TERRAMCP_ALLOW_NETWORK_PROXY Optional, local tooling only Set to 1 only in a trusted environment whose DNS proxy uses 198.18/15. Standard runs reject private/reserved destinations. Never enable for untrusted source checks.

Never write secrets to tracked files, candidates, reports or URLs. Set environment values through your shell/deployment secret store. .env* is ignored as a precaution; no env files are committed. The optional analytics beacon token is a public site identifier, not a secret.

Cloudflare D1

v1 uses D1: free within quotas, a native Worker binding, no extra signup, and adequate for a few thousand catalog rows. Neon/Postgres code and dependencies were removed. Drizzle’s SQLite schema is in src/db/schema.ts; generated SQL and snapshots are under migrations/. Arrays and provenance/score objects use JSON text columns.

The terramcp database is bound as DB in wrangler.jsonc. Its database ID is public configuration, not a credential. Export Cloudflare credentials in your shell, then:

npm run cf-typegen
npm run db:migrate
npm run db:seed

Migrations use Wrangler’s D1 migration tracking. scripts/seed-d1.ts generates escaped SQL from data/listings.json into ignored .wrangler/seed-d1.sql, applied remotely through Wrangler. Upserts preserve listing creation times; listings whose slugs are absent from JSON are deleted. Submissions and score history are retained, including history for removed listings. Apply migrations before seeding so the history-preserving schema is in place. Scoring updates committed JSON; history storage exists for future operational snapshots.

For local D1 preview:

npx wrangler d1 migrations apply terramcp --local
npx tsx scripts/seed-d1.ts
npx wrangler d1 execute terramcp --local --file=.wrangler/seed-d1.sql
npm run preview
# In another terminal:
MCP_TEST_URL=http://localhost:8787/mcp npm run test:mcp

Without a binding, reads use JSON and submissions return 503. A configured database failure propagates; writes never silently succeed. With next dev, OpenNext supplies a local D1 binding, so migrate and seed locally first. No remote credentials are required for local D1.

Directory & APIs

  • /: search, disciplines, highest scores, labelled sponsored coming-soon slots and sponsored consultancy CTA.
  • /listings: query, format, category, region and minimum-score filters. Regional filters also include Global sources.
  • /categories/{slug} and /listings/{slug}: collections, evidence, access requirements and score breakdowns.
  • /submit: editorial submission; no automatic publication or lead-email forwarding.
  • /about: methodology and agent connection details.
  • GET /api/listings?query=water&category=hydrology&region=England&min_score=50: JSON; placeholders excluded. Optional kind=mcp|skill|plugin|data_api|dataset|software. Invalid filters return 400.
  • POST /api/submissions: validated JSON fields name, url, kind, description, optional email; same-origin browser requests. DB required; success 201, unavailable 503.

Public submission has field/body bounds and a honeypot; enable free hosting-level rate limits before high-traffic use. Emails are private editorial records, not included in catalog JSON or MCP responses. Owner must provide a contact/privacy process before collecting personal data in production.

Remote MCP

Live endpoint: Streamable HTTP at https://terramcp.cjwhann.workers.dev/mcp (https://terramcp.earth/mcp after custom-domain setup) (local: http://localhost:3000/mcp). No authentication, Redis, session database or SSE session persistence required. Official @modelcontextprotocol/sdk Web Standard transport creates a fresh server per POST and returns JSON. GET/DELETE return 405 intentionally. /mcp rejects cross-origin browser Origin headers with HTTP 403 unless the exact origin appears in MCP_ALLOWED_ORIGINS. Same-origin requests and non-browser SDK clients without an Origin header can connect.

Tool Arguments Returns
search_sources query, optional kind, category, region, min_score, use_case, audience Separate sponsored placements and ranked organic sources with evidence
get_source slug Source with score, currency, access and provenance; unknown slug gives tool error
list_categories none Six disciplines and real-source counts
top_sources optional category, limit (1–50; default 10) Separate sponsored placements and highest organic evidence scores; limit applies to organic
get_consultant none Sponsored consultancy listing: contact URL, specialisms and CTA

SDK example (client configuration fields otherwise vary by app):

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const client = new Client({ name: "consultancy", version: "1.0.0" });
await client.connect(
  new StreamableHTTPClientTransport(new URL("https://terramcp.earth/mcp")),
);
const result = await client.callTool({
  name: "search_sources",
  arguments: { query: "river", category: "hydrology" },
});
await client.close();

The Aegaea and hydrometric.io listings are sponsored coming-soon placeholders, labelled Sponsored; their MCP servers do not yet exist. The Hydrometric website is https://hydrometric.io. Sponsored placements match configured category slugs and case-insensitive keyword words/phrases; when both category and query are supplied, both must match. Format, region and minimum-score filters also apply. Premium precedes standard within the paid group only. search_sources and top_sources return { sponsored: Listing[], organic: Listing[] }; all paid entries have sponsored: true, while placeholders remain unscored. /advertise explains tiers and disclosure. Discovery tools remain public; authenticated router tools now provide normalised upstream data. See docs/ROUTER.md.

Daily discovery & curation

.github/workflows/daily.yml schedules daily 06:17 UTC and supports workflow_dispatch. It checks out the repository default branch, targets PRs to that default branch, and uses a unique automation/daily-sources-{run_id}-{attempt} branch per run; it never pushes to main. A guard refuses an existing review branch; signed commits use the GitHub API to create the new ref, avoiding the action’s default force-push path. GitHub only enables scheduled workflows on the default branch, so cron will not activate while this file exists only on a build branch. Activating it requires a later owner-controlled default-branch/merge decision; this build does not change main or repository defaults. After activation, configure Actions “Allow GitHub Actions to create and approve pull requests” so GITHUB_TOKEN can create the review PR. Public standard runners are free.

npm run discover
npm run enrich
npm run score

Discovery reads Official MCP Registry /v0.1/servers with updated_since, resuming a bounded 20-page scan using a saved cursor; the watermark advances only once a cycle completes. Searches GitHub and npm for 11 engineering terms plus MCP, attempts public PyPI search and verifies distribution metadata, and diffs Sparkgeo README repository links against the last snapshot. PyPI has no supported full-text API and can serve a browser challenge: data/discovery-report.json explicitly reports this, without inventing results. Search results are untrusted pending candidates, including false positives. Initial observed run produced 262 candidates. Human review must establish relevance, licence and auth before adding a listing. State advances when review changes are merged; removals from Sparkgeo require review too.

Enrichment refreshes observed GitHub stars, actual last commit, archive state and licences; leaves unknown auth null. Keyword categorization is a suggestion only; no paid content API is used. Candidates never go into the live listings/database automatically. Reports/logs do not print keys or connection URIs. Source health checks fetch small public pages, not bulk datasets. Scoring methodology and caveats are in PLAN.md and /about.

Daily discovery, enrichment and scoring only update versioned JSON and never write to the live DB. .github/workflows/deploy.yml applies D1 migrations and syncs reviewed data/listings.json before deployment on push to main with CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID secrets. Missing credentials skip deployment and database sync.

Deploy & DNS for terramcp.earth

The Next.js site uses OpenNext Cloudflare, deployed as one Worker named terramcp. /mcp uses the official SDK’s WebStandardStreamableHTTPServerTransport inside the Worker with stateless JSON responses, all five tools and the exact Origin allow-list. D1 provides catalog and submission persistence; no Agents SDK, Durable Objects, R2 or paid bindings are needed. Next.js and eslint-config-next are pinned to 16.3.8 because the adapter currently fails at runtime with Next.js 16.4’s new preview-props manifest (upstream issue); recheck Workers preview before upgrading. Dynamic catalog pages use filtered, limited D1 queries; when D1 is unavailable, the catalog loads through the Workers static asset binding. prebuild/predev generate public/catalog.json from committed data so the large fallback is excluded from the Worker JavaScript.

D1 uses the native DB binding. Deployment needs no database connection secret.

npm ci
npm run cf-typegen
npm run preview
# In another terminal:
MCP_TEST_URL=http://localhost:8787/mcp npm run test:mcp

preview builds OpenNext and starts local wrangler dev (no deployment). To run an already built Worker: npx opennextjs-cloudflare build then npx wrangler dev. Generated bindings types and build artifacts are ignored. Type generation omits global runtime types so Node/DOM types remain consistent across the Next.js site and Node tooling. For local Next.js verification, supply secrets through shell environment variables; production uses Worker secret bindings. .dev.vars.example documents names only. Never put real secrets in any file.

When deployment is authorized and credentials are available, export CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in your shell (never put values in files), then:

npx opennextjs-cloudflare build
npm run db:migrate
npm run db:seed
npx wrangler deploy
# Equivalent build + deploy:
npm run deploy

Create an API token scoped to the target account with Workers Scripts Edit and D1 Edit; custom-domain setup also requires Zone Read and DNS Edit on terramcp.earth. Get the account ID from the Cloudflare dashboard. Keep the account on Workers Free. Set MCP_ALLOWED_ORIGINS as an optional Worker variable. The database binding is configured in Wrangler; no database secret is needed at build time.

.github/workflows/deploy.yml deploys only pushes to release/** (see docs/RELEASE.md) using cloudflare/wrangler-action@v3 and repository secrets CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID. It skips cleanly if either is absent. Adding this workflow on build/cloudflare does not deploy; merging later is an owner decision. The authorized branch deployment publishes to workers.dev; custom-domain DNS setup remains a separate operator step.

Cloudflare Workers Free allows commercial use. Its limits include 100,000 Worker requests/day, 10 ms CPU/request, 128 MB memory and a 3 MiB compressed Worker bundle. Network wait does not consume CPU; static asset requests are free. Local validation does not establish production CPU compliance: monitor real traffic and simplify rendering if the free CPU budget is exceeded. No automatic paid upgrade is configured.

Move DNS from Porkbun and add the Worker domain

  1. In Cloudflare, Add a domain / Add site, enter terramcp.earth and select the Free plan. Review imported DNS records, preserving all MX/TXT and other mail/service records. Record the two nameservers assigned to this specific zone, both ending in *.ns.cloudflare.com.
  2. Before changing nameservers, log in to Porkbun > Domain Management > terramcp.earth > Details > DNSSEC. Disable Porkbun DNSSEC/remove the existing DS records and allow that change to propagate. An old DS record can break resolution after the nameserver move.
  3. In Porkbun Domain Management > Details > Nameservers, replace the existing nameservers with exactly the two assigned Cloudflare nameservers and save. The domain remains registered at Porkbun. Do not use generic Cloudflare or Porkbun’s own Cloudflare-backed nameservers. See Porkbun nameserver instructions.
  4. Wait until Cloudflare shows the zone as Active. Deploy the Worker when authorized. In Cloudflare Workers & Pages > terramcp > Domains > Add Domain > Custom Domain, enter terramcp.earth. Cloudflare provisions the DNS record and certificate; remove a conflicting apex web record if requested, preserving mail records. Alternatively uncomment the ready routes entry in wrangler.jsonc and deploy. See Workers custom domains.
  5. Confirm HTTPS, homepage, listings, categories, /api/listings and MCP_TEST_URL=https://terramcp.earth/mcp npm run test:mcp. www needs its own domain/redirect if desired. Optionally enable Cloudflare DNSSEC after activation and install its new DS record at Porkbun; never reuse the old DS record.

Submission spam controls silently accept filled honeypots without saving. DB-backed submissions are capped at 5 per hashed IP and 200 globally over the rolling last 24 hours (HTTP 429). Cloudflare supplies cf-connecting-ip; other hosting proxies must overwrite x-forwarded-for; missing or invalid IPs share a bucket. Raw IP addresses are not stored.

For data APIs without repositories, a successful page load is labelled page reachable, earns no maintenance points, and caps currency at C unless source.last_updated contains an editorially verified upstream update timestamp. HTTP checks do not verify API functionality.

scripts/bootstrap-seed.ts refuses to overwrite curated data unless explicitly invoked with --force.

Search responses (search_sources and /api/listings) contain slim summaries and pagination metadata: count, listings, limit, offset, next_offset. The default limit is 20, maximum 50; pass offset=next_offset for the next page. Retrieve full provenance and breakdown through get_source. Health breakdowns distinguish checked from inherited; only timeout/deadline deferrals retain previous health, and deferred rows retain their updated timestamp. Registry imports carry source.registry.ingested; re-runs refresh imported metadata and enrichment while preserving existing health, scores and timestamps, and remove missing/unverified imports while retaining curated rows.

Listing enrichment is owned by the daily research registry. See the enrichment schema and daily ingest workflow. Run REGISTRY_PATH=/workspace/terramcp-research/registry.json npm run ingest:registry on the box, review the JSON changes, then commit and submit a PR.

Growth and measurement

/use-cases links enrichment-derived collections. Category and use-case landing pages require at least three distinct real listings; placeholders never count. Exact phrases and explicit keyword themes are grouped without changing enrichment or quality scores. Audience tags supply the page’s audience context, not extra eligibility votes. Themes use use-case text only; the LiDAR/flood theme requires both concepts in the same phrase. Unique phrases do not produce thin pages. /sitemap.xml, /robots.txt, the shared Open Graph image, listing/collection JSON-LD, /feed.xml (latest 50 by creation time) and /changelog provide discovery metadata. RSS and digest use creation time, not enrichment updates.

Run npm run digest (optional positional ISO timestamp) to write the last seven days, end exclusive, to out/digest/YYYY-MM-DD.md and .html. HTML uses inline CSS for email. It reads committed data, never sends, and overwrites the same dated output on reruns. out/ is ignored. Distribution metadata and owner-only publication steps: docs/DISTRIBUTION.md. Nothing is submitted automatically.

Variable Required? Purpose
ADMIN_TOKEN Metrics access only Secret bearer credential, set with Worker secrets or local environment. Never place in a URL or public-prefixed variable. Missing token denies all access.
NEXT_PUBLIC_CF_BEACON_TOKEN Optional Public Cloudflare Web Analytics site token. Beacon is omitted when unset. Set at build/runtime as appropriate; disable automatic beacon injection to avoid duplicates.
CATALOG_JSON_ONLY Optional local verification 1 bypasses D1 for catalog/MCP reads; event writes are unavailable too.

Apply new migration 0008_event_counters.sql before enabling metrics. Counters store only UTC day, event name, fixed dimension and count, using atomic D1 upserts. No PII, IPs, cookies, query strings, request bodies, referrers or user identifiers are stored. Browser page views include client navigation; outbound and consultant clicks use delegated link listeners. Downloading the MCP connection file counts install intent; successful MCP initialize and tool operations count separately. Counts are not unique users or confirmed client installations and may include bots, repeated clicks and registry scans. Public telemetry requires same-origin JSON, a small body and allowlisted fields; an atomic D1 cap admits at most 10,000 public events per UTC day. D1 event failures never break MCP responses; writes run through ctx.waitUntil. The metrics dashboard also accepts HTTP Basic auth (any username, ADMIN_TOKEN as password); URL tokens are never accepted. Missing D1 returns 503 from telemetry/metrics and retains no in-memory substitute.

Sponsor checkout records checkout_start after a Stripe session is created and checkout_complete after a verified paid completion is applied, deduplicated by Stripe event ID. Successful discovery and authenticated router tool calls are counted once through MCP response inspection. Telemetry failures do not fail checkout or webhooks.

GET /api/metrics requires Authorization: Bearer . GET /admin/metrics accepts Bearer or HTTP Basic auth (any username, ADMIN_TOKEN as password), allowing browser login. Both send no-store responses; no query-token login or cookies are used. Responses include daily and Monday-start weekly counters for the last 90 UTC days; boundary weeks are partial. Example using an environment variable: curl -H "Authorization: Bearer $ADMIN_TOKEN" http://localhost:3000/api/metrics. Empty/missing credentials deny access. Metrics retain daily aggregates until the operator chooses a retention policy; the dashboard reads only the bounded window.

TerraMCP router v2

/account uses email magic links to manage TerraMCP API keys and encrypted upstream keys. Eight authenticated tools on /mcp cover EA flood monitoring, EA Hydrology, NRFA, USGS and OpenTopography BYO keys. Discovery remains public. See docs/ROUTER.md for tool arguments, provenance, cache TTLs, limits, migration and operational details.

Secret names (environment/Worker secrets only): SESSION_SECRET (32+ characters), VAULT_KEY (base64url 32-byte AES key), RESEND_API_KEY, optional USGS_API_KEY, and optional STRIPE_SECRET_KEY. Configure AUTH_EMAIL_FROM and canonical AUTH_BASE_URL. Explicit AUTH_DEV_LOG=1 logs magic links only on localhost outside production; production never logs a token. Free quota: 1,000 calls per account/UTC calendar month; ROUTER_RATE_PER_MINUTE=30 per key. .dev.vars.example lists empty secret placeholders; do not put real values in tracked files. Pro and Team use flat Stripe subscriptions; see /pricing and Stripe setup.

npm run smoke:router hits the real free upstream APIs. Optional ROUTER_SMOKE_BASE_URL and shell-only TERRAMCP_API_KEY also exercise deployed router calls. OpenTopography is covered by a synthetic GeoTIFF contract fixture until a user’s BYO key is available for a live recording.

Self-serve sponsorship

/advertise offers approved pricing (£29 one-off Verified, £99/month Featured, £249/month Category Partner), a sponsor form and Stripe-hosted subscription checkout. /admin/sponsors provides protected moderation; /api/stripe/webhook verifies raw-body signatures and updates D1 billing state without changing quality scores. Billing tables survive catalog reseeding. See docs/STRIPE.md for the exact owner setup, webhook events, moderation policy and sandbox verification steps.

Environment / Worker secret names: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_VERIFIED, STRIPE_PRICE_FEATURED, STRIPE_PRICE_PARTNER, SPONSOR_SITE_URL (trusted origin) and ADMIN_TOKEN (moderation authentication). Names-only placeholders are in .dev.vars.example; never put actual secrets in tracked files. No Stripe publishable key or Node SDK is required. Missing configuration disables checkout gracefully. Products/prices can be created with npx tsx scripts/stripe-setup.ts using a shell-exported test key; live keys are refused without --live.

Guides

Guides live in content/guides/*.md with a filename slug and plain-text front matter:

---
title: A useful guide
description: What the reader will learn.
author: Chris Cameron-Hann
published: 2026-10-15
updated: 2026-10-15
---

## First section

A paragraph with a [source](/listings/ea-hydrology-api).

The renderer supports paragraphs, second/third-level headings, bullet lists and links (HTTPS or local paths). Raw HTML is escaped. Run npm run guides:build after editing to refresh the committed data/guides.generated.json bundle; development, build and test hooks also refresh it. Pages and /guides/feed.xml use that bundle, with no filesystem access in the Cloudflare runtime. Published guides appear on /guides and in the sitemap. Dates are UTC calendar dates.

Run npm run guides:weekly -- --date=2026-10-15 to generate a weekly registry post and refresh the bundle. Without --date, it uses today’s UTC date. The comparison covers seven calendar days ending on that date, using committed data/listings.json snapshots from Git history. Use a full checkout with history; a shallow checkout can produce an incomplete baseline. An absent baseline is explicitly labelled an initial inventory, not a list of upstream launches. The generator includes non-placeholder geo/water/environment/climate MCP records, ignores timestamp-only changes and shows up to 15 items per change category with counts and immutable snapshot links for the full evidence. Review discovery tags and content before committing a generated post. Re-running for the same date and registry history replaces the same Markdown file deterministically.

Free discovery tools

  • /tools/picker ranks sources from data/listings.json for seven engineering and environmental job types and a selected region. Category and description relevance lead the ranking, followed by coverage and quality. Global and UK-wide coverage can match narrower regions; country-only coverage is never expanded to neighbouring countries. Each result explains its match.
  • /tools/badge provides copyable README Markdown for every real listing. /badge/.svg renders its catalog quality score with a one-hour browser cache and a one-day shared cache; unknown or placeholder listings return 404.
  • /status shows recorded MCP health checks with their target URLs, HTTP results and timestamps. Evidence older than 48 hours (relative to the request) is stale. Repository HTTP checks do not imply a successful MCP handshake. Catalog updates require a check and deployment.

These tools read the same catalog as the directory (D1, falling back to the catalog.json asset), so the JSON is never bundled into the Worker. They require no account and make no new network probes. They are linked from the main navigation and sitemap. Feature tests live in scripts/free-tools.test.ts.

View this README on GitHub

安装

npx tsc --noEmit