Skip to main content
Glama
Reppesgaard-Ninja

marketing-stack

marketing-stack

An orchestration layer built on Claude Code and MCP. One entry point, three namespaces, nine tools — over data that previously lived in scattered JSON files, logs and weekly report emails. Anyone using the server needs no knowledge of the directory layout. No SSH, no reading JSON by hand.

Deutsche Fassung

Three bodies of data are connected:

  • Terminology — lookup and prose checking against an approved DE-EN glossary

  • SEO / GEO — search performance, ranking decay, share of answer in AI responses, opportunities, competitors, change history

  • Foundation files — binding company facts and the blocklist of impermissible claims, exposed as MCP resources and therefore in effect in every session

The governing rule across the whole server: no figure without its date, and no absence that could be mistaken for a measurement.

At a glance

Protocol

MCP over stdio, JSON-RPC 2.0

Implementation

@modelcontextprotocol/sdk, Node.js

Scope

3 namespaces, 9 tools, 2 resources

Client

Claude Code

Upstream data source

a custom-built SEO/GEO bot (Node.js, process management via PM2, seven independent detectors) integrating Google Search Console, the Webflow CMS API and four LLM APIs

Write path

none outside the project directory

Network at runtime

none

Open ports

none

Secret access

none — the server never reads .env

The API integrations live in the upstream bot, not in this server. At runtime the server calls nothing; it reads local files and formats the result. That separation is deliberate and part of the security model.

Related MCP server: mcp-deepcontext

Context

This repository is an extract from a system running in production, not a sample project. The server runs under Claude Code on an Ubuntu VPS and reads the output of a custom-built SEO/GEO bot that analyses a company website, generates suggestions, and writes them once approved.

What is published is the transferable layer: server logic, tool boundaries, security model, limits. Not published: operational data, installation-specific paths, and the blocklist containing reference customer names — that file is loaded at runtime from outside the repository (see Limits, item 11).

The problem it solves

The upstream bot produces data continuously. Seven detectors write independently into their own files and each sends its own report. No layer brings their results together.

So the question "how is this one page doing?" used to cost: log in, locate the most recent file across several directories, read JSON by hand, search the change log for the last write, assemble the values mentally. Five files for one question — manual work nobody repeats across dozens of pages.

seo_page_visibility consolidates nine signals from five sources, each with its source and data date, and explicitly names what is not available for that page. The difference is not convenience but scalability: manual work becomes a question.

What the server explicitly does not replace: the dashboard and the periodic reports. Those answer questions defined weeks ago. This server answers questions that come up now.

Foundation files as MCP resources

The fact base and the blocklists previously took effect only inside the bot: when generating meta descriptions and during the fact check before every write. A hand-written post, an ad, a slide deck — none of them were covered.

Exposed as MCP resources (facts://company, facts://guardrails) they apply in every Claude session without anyone pasting them in. The rule set that prevents a silver partner status from quietly becoming gold, or a certification number from being invented, now takes effect where the text is actually written — not only in the one system that knows about it.

The mechanism is a one-way export, not a reimplementation: the bot's source modules are exported daily as data (JSON, text extract), never loaded via require. When the source changes, the resource changes on the next run. If the export fails, the previous file stays in place and the run reports an error — rather than silently serving an empty list.

Hence the project's hardest rule: the server must never become a second source of truth. Where logic exists in the production system it is mirrored here, not reinvented. Where mirroring is impossible, the gap is named in the output itself (see Limits, item 6).

The architectural decision: mirror, not direct access

The server does not read from the production directories but from a mirrored copy.

  ┌──────────────────────────────────┐
  │  production SEO/GEO bot          │   runs as root
  │  data/  changes.log  lib/        │   seven detectors, writing continuously
  └────────────────┬─────────────────┘
                   │
                   │  mirror cron, daily 09:00, runs as root
                   │  rsync (copies) + exports (JS modules → JSON)
                   ▼
  ┌──────────────────────────────────┐
  │  mirror                          │   owned by the unprivileged user
  │  data/  changes.log              │
  │  companyFacts.json  guardrails   │
  └────────────────┬─────────────────┘
                   │  read-only
                   ▼
  ┌──────────────────────────────────┐      ┌──────────────────────────┐
  │  marketing-stack (this server)   │◀─────┤  terminology glossary    │
  │  server.js + lib/                │ read │  (JSON)                  │
  └────────────────┬─────────────────┘      └──────────────────────────┘
                   │  stdio / JSON-RPC 2.0
                   ▼
             Claude Code

The dependency runs strictly one way: the MCP server depends on the bot; the bot does not depend on the MCP server. There is not one line in the bot that refers to this server.

The rejected alternative was direct access to the production directories — always current, no mirror needed. The price would have been an MCP process running with root privileges alongside a production system, and Claude Code having to operate as root. A bug in the server could then have reached production data — even with read-only intent.

The mirror was chosen instead. The production system is therefore structurally untouchable: not because the server code behaves well, but because the process cannot reach those files with its privileges.

The price is data age — up to 24 hours. So that this never goes unnoticed, every signal states its data date, derived from the filename and falling back to mtime only when there is no alternative. Past a threshold, every tool response is prefixed with a staleness banner. A second effect of the same decision: a detector writing in the middle of a query cannot hand over a half-written file.

Failure behaviour: if the mirror cron stops, data ages and the banners appear. If the bot stops entirely, the server keeps answering from the last mirror, with a banner. If the server fails, the bot does not notice. If the server returns a wrong number, the error stays on the read path — it can cause a bad decision, but not data damage.

Findings

A query layer over scattered data inevitably exposes divergences between documentation and reality. Two of them on the day it was finished:

535 self-contradictions in an approved glossary

A curated corpus of 4,838 terms was considered complete after seven review rounds. This server's checking function found 535 terms held simultaneously as a rejected variant and as an approved equivalent.

The cause is structural and transferable: the automated QA pass checks the glossary against itself in blocks of 30 terms. Across 4,838 entries that is roughly 160 blocks. Two contradictory entries sitting in different blocks are never compared — the probability that two given entries land in the same block is below one percent. A block-wise model call cannot form a corpus-wide set intersection. The human rounds could not find it either, because all of them worked on subsets: each reviewer saw one correct entry and decided correctly. The contradiction exists only in the aggregate view.

This is not an argument against the quality of the curation but for a missing layer of verification. The server introduced it without that being its purpose — and deliberately does not resolve the finding: the corpus is a record of review, and a script that quietly rewrites approved entries makes it impossible afterwards to distinguish what a human verified from what an automation asserted. The server classifies, counts and documents it as a property of the data source — surfaced as the class WIDERSPRUCH (contradiction).

A module path that had survived three revisions of the documentation

The fact base was consistently documented under a path that does not exist; the file sits in a different directory. It surfaced because a mirror command pointed at the documented path and failed silently — precisely the class of error the whole server is built against. A second module, the only consumer of that fact base, appeared in no module overview.

A third finding concerned a locale asymmetry: two visibility signals for the same page pointed in opposite directions. In an aggregate figure both would have been invisible — and that is exactly how they had been reported. Both signals are now separated by language.

Namespaces and tools

Terminology

term_lookup(term, richtung) — Looks up a term. richtung: de-en, en-de or auto (default; searches both directions, because the glossary holds both DE and EN source entries). Returns the binding translation, rejected variants, rationale, and do-not-translate status.

term_check_text(text, modus) — Checks DE/EN prose against the glossary. Every hit is classified; the order of checks is binding, because each class implies a different action:

Class

Meaning

What the author can do

WIDERSPRUCH

rejected and approved at once

nothing — this needs a terminology review

VERWORFEN

held only as a rejected variant

replace with the stated equivalent

MEHRDEUTIG

several distinct approved targets

decide by context

EINDEUTIG

exactly one target, used correctly

nothing (shown only with modus: alle)

modus: probleme (default — problem classes only, plus a count of suppressed hits) or alle.

Deliberately not built in: a minimum-length or stopword filter. It would silently swallow real findings such as approval or documents. Noise is reduced through precision only — word boundaries, longest match per position, consolidation of identical targets — never through suppression. A checking tool that stays quiet is worse than one that says too much.

term_do_not_translate() — All protected names that must remain unchanged.

SEO / GEO

seo_page_visibility(seite) — The centrepiece. Consolidates nine signals for one page (slug or substring) from five sources: search performance, impressions without clicks, decay, current rankings, anomalies, AI impressions (pilot pages only, split DE/EN), mentions per AI engine with prompt language, meta status and provenance (bot or manual), and the last write. Every line names its source and data date; where a signal is missing it reads "no data in <source>".

geo_status(wochen) — Share of answer per AI engine across the last N runs, separated by language (DE / EN / overall), trend against the previous run, prompts won and lost.

seo_opportunities(typ) — Opportunities by type (striking_distance, impressions_no_clicks, decay), sorted by an explicitly named field, top 15 with the total count stated.

geo_competitors(name) — Competitors appearing in AI answers, with trend. Without name: all competitors in the latest run by frequency. With name: a time series across all runs.

seo_change_history(seite) — What the bot wrote, when, and on which page. Without seite: the last 20 changes.

Foundation files

facts_for_url(url) — Maps a URL to a business unit (substring match on product abbreviations, first match wins; no match → null, and then no facts). Returns the matched abbreviation as matchedProduct so that a false positive becomes visible instead of invisible.

Resources: facts://company (unit facts and usage rules) and facts://guardrails (prohibited claims).

Security model

Guardrail

Implementation

Strictly read-only

No write outside the project directory. Source files are opened for reading only.

No guessed paths or field names

Every file was read before code processed it. The basis is a preceding schema survey, not an assumption about structure.

No secrets

No access to .env. A scan across all source files returned nothing.

No network calls at runtime

The server reads local files. No outbound calls.

No execution of bot code

Fact base and blocklists are exported as data, never loaded via require.

Privilege separation

What root owns stays with root. The server process cannot reach the production files.

Registration in Claude Code

  1. Server entry in the project .mcp.json (start command plus environment variables). Template: .mcp.json.example.

  2. Approval via enabledMcpjsonServers in the user settings — otherwise a project server stays permanently "pending approval".

  3. Binding to the start command via allowedMcpServers using serverCommand, not serverName.

Item 3 matters most: serverName is a freely chosen label and not a security control. Anyone who knows the name can assign it. Only binding to the concrete serverCommand guarantees that the vetted start command is what actually runs.

Approval in the user settings remains self-imposed: whoever set it can loosen it. Real enforcement only exists once the rule sits somewhere the executing user cannot reach — a managed policy with allowManagedMcpServersOnly. Allowlists from user, project and local settings are then ignored, while denylists continue to apply from all sources. For single-user operation this is not operationally necessary; it turns "I abide by my own allowlist" into "policy and execution are separate".

Limits

This is the most important section of the documentation. One principle runs through it: anywhere an absence could look like a measurement, it is labelled explicitly. Not tracked rather than 0. No anomaly rather than data missing. No claim permitted rather than unknown. Partial list rather than complete list. Blocklist unavailable rather than empty blocklist.

  1. Data age. The mirror is refreshed daily. Every signal states its data date (from the filename, otherwise mtime). Once the newest data date passes the threshold, every tool response is prefixed with a staleness banner.

  2. Contradictory glossary. A substantial share of terms is held simultaneously as a rejected variant and as an approved equivalent (reported as a concrete number at startup). This is a property of the data source, not resolved by the server but surfaced as the class WIDERSPRUCH — never quietly "corrected".

  3. DE/EN duplication. Many terms exist twice, as a DE and as an EN source entry. term_check_text consolidates per position: same position plus same target yields one message; several synonyms mapping to the same approved form yield EINDEUTIG; only genuinely different targets remain MEHRDEUTIG.

  4. term_check_text finds only what is in the glossary. A wrong term not held there as a rejected variant will not be detected. The tool supplements review; it does not replace it.

  5. facts_for_url matches by substring. Short product abbreviations can hit URLs unrelated to the product. Every match therefore returns the matched abbreviation as matchedProduct.

  6. EXCLUDE_URLS is only a partial list. Further exclusions (locale variants, utility pages, strategic exceptions) live outside the mirror and are unavailable to this server. Only what is in the file is reported.

  7. null for supportFact / volumeFact means "no claim permitted" — not "unknown" and not "look it up elsewhere". The server never fills such fields.

  8. AI impressions exist for the pilot pages only (split DE/EN). For any other page the answer is "not tracked" — not 0.

  9. Empty ranking_anomalies means "no anomaly", not "data missing".

  10. Local files only. No network calls, no writes outside the project directory.

  11. The reference customer blocklist is not part of this repository. It is loaded at runtime from a separate file (path via environment variable, pattern in .mcp.json.example). If the file is absent, facts://guardrails does not quietly return an empty list but states the gap explicitly — the rule "name no customers" remains in force, plus a warning on stderr.

  12. The server must never become a second source of truth. Where logic exists in the production system it is mirrored here, not reinvented. Where mirroring is impossible, the gap is named in the output.

Operation

Environment variables

Variable

Purpose

MARKETING_STACK_DICT

Path to the glossary file. If unset: the newest woerterbuch_komplett_*.json in the terminology output directory.

MARKETING_STACK_MIRROR

Root of the SEO mirror (contains data/, changes.log, companyFacts.json).

MARKETING_STACK_FACTS

Optional direct path to companyFacts.json (otherwise derived from …_MIRROR).

All source paths come from environment variables — no path is compiled in. Concrete values for a given installation are not part of this repository; .mcp.json.example serves as a template with generic paths.

Start

node server.js

On startup the server reports on stderr, among other things, the age of the mirror and the number of glossary contradictions. The glossary is loaded once at startup; the SEO/GEO files are read per call.

Health check

claude mcp list

Run as a privileged user or from another directory, this command systematically returns nothing — and that looks exactly like a broken server. Always run it as the user owning the mirror, and from the project directory.

Tests

node test/driver.js       # stdio session across all tools
node test/diagnose.js     # diagnostics for term_check_text match logic (read-only)

Data sources → tools

Source

feeds

terminology glossary (woerterbuch_komplett_*.json)

term_lookup, term_check_text, term_do_not_translate

data/striking_distance_*

seo_opportunities, seo_page_visibility

data/impressions_no_clicks_*

seo_opportunities, seo_page_visibility

data/decay_*

seo_opportunities, seo_page_visibility

data/ranking_snapshots/snapshot_*

seo_page_visibility

data/ranking_anomalies_*

seo_page_visibility (as a signal, no separate tool)

data/geo_monitor_*

geo_status, geo_competitors, seo_page_visibility

data/gsc_ai_baseline.json

seo_page_visibility (AI impressions, pilot pages)

data/site_inventory_latest.json + data/seo-origins.json

seo_page_visibility (meta status, provenance)

changes.log

seo_change_history, seo_page_visibility

companyFacts.json

facts://company, facts_for_url

companyFacts_source.txt

facts://company (rule texts)

factcheck_guardrails.txt

facts://guardrails

No tool was built on a guessed data shape.

F
license - not found
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    C
    quality
    D
    maintenance
    An MCP server that enables persistent memory, structured thinking sessions, and project-based knowledge management for Claude. It includes specialized coding tools for package discovery and reinvention prevention by validating code against existing libraries and APIs.
    Last updated
    17
    3
  • A
    license
    -
    quality
    D
    maintenance
    MCP server enabling symbol-aware semantic search in Claude Code, allowing precise location of functions, types, and implementations via a symbol graph and embeddings.
    Last updated
    9
    MIT
  • F
    license
    A
    quality
    C
    maintenance
    MCP server for the ONE Framework ERP that enables Claude to explore data models, run queries, and understand business logic through autonomous access.
    Last updated
    10

View all related MCP servers

Related MCP Connectors

  • MCP server giving Claude AI access to 22+ NYC public-record databases for real estate due diligence

  • Augments MCP Server - A comprehensive framework documentation provider for Claude Code

  • Hosted Amazon Seller and Vendor MCP server for Claude, ChatGPT, Cursor, Codex, Gemini, Copilot.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Reppesgaard-Ninja/marketing-stack-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server