senegal-mohebs-tlm-server
A shared memory MCP server for the Senegalese MOHEBS teaching and learning materials pipeline that ensures AI-generated documents remain consistent in characters, terminology, and concept coverage while varying example domains, operating across grade and subject via Firebase storage.
README
senegal-mohebs-tlm-server
An MCP server that gives the Senegalese MOHEBS teaching & learning materials pipeline a shared memory layer — so AI-generated documents stay consistent (characters, terminology, concept coverage) and deliberately varied (rotating example domains — fruits → legumes → …), across any grade and subject.
The server works on one grade + subject at a time (e.g. ci / maths). You pick the pair with set_context; that choice selects which local sources load and which Firebase namespace the documents and history live under. Until a pair is set, the source- and bucket-dependent tools return a short "choose a grade/subject first" prompt instead of running.
Firebase Storage is the shared source of truth for generated documents and the history file. The generating agent, the server, and you no longer need to share a disk: everything reads and writes the same bucket. Sources (knowledge graph, terminology, prompts) stay local to the server as read-only inputs you edit in place.
What lives where
| Thing | Location |
|---|---|
| Knowledge graph, terminology, the two prompts | Local sources/<grade>/<subject>/ (you edit these) |
Generated .docx (chapter manuals + lesson sheets) |
Firebase <grade>/<subject>/documents/chapitre_NN/… |
History / tracker (history.json) |
Firebase <grade>/<subject>/history.json |
Object hashing uses the GCS object md5 from metadata — the server never hashes a local file, which is what removes the cross-host mismatch that broke log_generation before.
Sources layout
Each grade/subject is a folder holding the same canonical filenames:
sources/
ci/
maths/
knowledge_graph.json
terminology.json
PROMPT_generate_chapter.md
PROMPT_generate_lessons.md
example_domains.json # optional; falls back to a built-in pool
ce1/
lecture/
…
get_context discovers these by scanning the tree, so the installed pairs are whatever folders exist.
Note. Dropping in a folder provides the data for a grade/subject. Wiring it up so the tools actually work also needs a registered profile (and, if its knowledge-graph shape differs from an existing subject, a curriculum adapter). A folder with no registered profile is rejected by
set_context. See Adding a new grade/subject.
Configuration
Required:
SERVICE_ACCOUNT_KEY_PATH— path to your Firebase service-account JSON (used for auth and for signing upload URLs).FIREBASE_STORAGE_BUCKET— the bucket name, e.g.your-project.appspot.com.
Optional:
TLM_GRADE/TLM_SUBJECT— pre-select the active grade/subject at startup, so you don't have to callset_contextfirst. Must match an installed folder pair.TLM_BUCKET_PREFIX— put everything under a prefix (e.g.pilot→pilot/ci/maths/documents/…,pilot/ci/maths/history.json). The grade/subject scope is always appended after the prefix.TLM_SOURCES_DIR— relocate the sources root (defaults to./sources). The per-subject filenames inside each<grade>/<subject>folder are fixed conventions:knowledge_graph.json,terminology.json, optionalexample_domains.json, and the prompt.mdfiles (each subject's profile names its own prompt files viaDeliverableSpec.promptFile).TLM_DOMAIN_NEIGHBORHOOD_K— how many chapters on each side count as a chapter's "neighborhood" for example-domain variety (default3).get_generation_contextonly reports the domains used by chapters within ±K of the target (by chapter number), and its fresh-domain suggestion avoids anything in that window. Larger K = stronger variety across a wider span; the payload stays bounded by the window regardless of how many chapters are authored.
Bucket layout
gs://<FIREBASE_STORAGE_BUCKET>/
<grade>/<subject>/
documents/
chapitre_05/<Manuel …>.docx
chapitre_05/<Fiches de leçons …>.docx
history.json
Document identity is scope:deliverable (e.g. 5:manual, 5:lessons) within a grade/subject; the scope is the first integer in the subfolder name, and the active subject's profile classifies the filename into a deliverable (for maths: a file named "Fiches de leçons …" is the lesson-sheets doc, anything else is the manual).
The generation flow (cross-host, no shared disk)
set_context(grade, subject)— pick what you're working on.get_contextlists the installed pairs and the current selection.get_generation_context(unit, deliverable)— curriculum slice, established characters, terminology guidance, coverage, and (for the teacher guide) the manual to build on.unitis the scope value (for maths, the chapter number) anddeliverableis a deliverable key (manual/lessons). For example-domain variety it returnsexampleDomains: { suggested, avoidNearby }:suggestedis a fresh object family to use, andavoidNearbymaps each nearby chapter number (within ±TLM_DOMAIN_NEIGHBORHOOD_K) to the domains it used — so adjacent chapters don't repeat the same family. This is a bounded window, not the whole book; usedomain_usagefor the full log.- Generate the
.docx. create_upload_url(relPath, confirm)→ the server returns a short-lived signed URL. Upload the file with an HTTPPUT(Content-Typeapplication/vnd.openxmlformats-officedocument.wordprocessingml.document). No large payloads go through the MCP channel. Requires confirmation — see below.log_generation(unit, deliverable, relPath, content, confirm)— the server reads the uploaded object's md5 from storage and records what you produced. History updated; no local file needed. Requires confirmation — see below.
Confirmation gate. The three tools that write outward —
create_upload_url(gates the upload),log_generation, andrecord_document_content— never act without approval, using the strongest gate the client supports:
- Client supports MCP elicitation → the server asks the user directly via an elicitation dialog. This is a hard gate: the agent cannot bypass it (even passing
confirm: truewon't skip it — a declined dialog blocks the action).- Otherwise → an agent-mediated two-step: the first call performs no side effect and returns
{ needsConfirmation: true, message }; the agent asks the user, then re-calls withconfirm: true.Input validation (e.g. unknown deliverable) runs before the gate, so bad calls fail first. All read-only tools are ungated. Note: in a fully headless run (no user, no elicitation) these tools cannot get approval by design — drive them only where a human is reachable.
Ingesting a doc authored elsewhere (e.g. an expert wrote chapter 2)
- The file is in the bucket (uploaded any way you like), under the grade/subject's
documents/. reconcilesurfaces it as untracked.get_document_text(relPath)returns its plain text (server downloads from the bucket and extracts via mammoth — it never calls an LLM).- Extract the structured content and call
record_document_content(...)(requires confirmation — call withconfirm: trueafter the user approves). Tracked from then on.
Reconciliation
Run on startup (when a context is active) and via the reconcile tool: present + md5 matches history → tracked (skipped); new/changed md5 → untracked (needs ingestion); in history but gone from the bucket → dropped; duplicates for one identity → the object matching the tracked md5 wins, else most-recently-updated.
Tools
Context (subject-agnostic): set_context, get_context.
Subject-agnostic — work the same for any grade/subject: get_terminology, terminology_sections, get_prompt, reconcile, list_documents, create_upload_url, create_download_url, get_document_text.
Subject-specific payloads — generically named, but what they accept/return is shaped by the active subject's profile:
list_units,get_curriculum,get_generation_context,record_document_content,log_generation. These take aunit(the subject's scope value — a chapter for maths, a week for CE1 reading) and, where relevant, adeliverablekey. The shapes are subject-specific: maths returnschapitreNum/leconNumetc., and thecontentpayload (characters, example domains, amorce/bilan) follows the maths storybook model — all fields optional.- Capability-specific (
exampleDomainRotation, maths only) —suggest_fresh_domain,domain_usage. Example-domain rotation is a maths storybook feature; they are gated on the capability, so for a subject whose profile doesn't enable it they return anotApplicablemessage instead of running.
Setup
npm install
npm run build
Wiring into a host (e.g. Claude Desktop)
{
"mcpServers": {
"senegal-mohebs-tlm": {
"command": "node",
"args": ["/absolute/path/to/dist/index.js"],
"env": {
"SERVICE_ACCOUNT_KEY_PATH": "/absolute/path/to/serviceAccount.json",
"FIREBASE_STORAGE_BUCKET": "your-project.appspot.com",
"TLM_SOURCES_DIR": "/absolute/path/to/sources",
"TLM_GRADE": "ci",
"TLM_SUBJECT": "maths"
}
}
}
}
TLM_GRADE/TLM_SUBJECT are optional — omit them and the agent picks a pair with set_context at the start of a session.
Architecture
The server supports many grades/subjects whose curriculum graphs and deliverables genuinely differ (CI maths is a graph[] of Chapitre/OS nodes; CE1 reading is nodes/relationships with a hasChild tree and no chapters). Behaviour is therefore pluggable per subject, not hard-coded:
- Curriculum adapter (
src/curriculum/adapters/*) — parses a subject's raw knowledge graph into a normalizedCurriculumModel, and exposes adetect()guard so a graph that doesn't match its schema is refused atset_contextinstead of silently mis-parsed. - Subject profile (
src/profiles/*) — declares the subject's deliverables (filenames, scope, dependencies), its capabilities, and how to build the pre-generation context. A registry insrc/profiles/index.tsbinds each(grade, subject)to its profile.
Modules are layered, and imports only ever point down. A build-time check (npm run check:cycles, run automatically by npm run build) fails on any import cycle:
app server/* · index.ts · activate.ts
profiles profiles/* — compose the services below, per subject
services storage/* · curriculum/* · generation/* — never import profiles
core config.ts · types.ts · context/{state,shared} · utils/* — leaves
Cross-module imports go through each module's index.ts (barrel); files inside a module import their siblings directly. activate.ts (resolve the profile → run the schema guard → bind the context) is app-layer glue that wires context/ to profiles/, so it lives at the root next to index.ts rather than inside the leaf context/ module. The full design rationale is in docs/multi-subject-architecture.md.
Adding a new grade/subject
Adding a subject takes its sources (data) and a profile (code). If the knowledge-graph shape differs from every existing adapter, it also needs a new adapter.
-
Drop in the sources under
sources/<grade>/<subject>/:knowledge_graph.json,terminology.json, the generation prompt(s), and optionallyexample_domains.json. -
Reuse or write a curriculum adapter (
src/curriculum/adapters/):- Same graph shape as an existing subject (e.g. the CI-maths
graph[]envelope) → reuse that adapter. - Different shape → add
src/curriculum/adapters/<name>.tsimplementingCurriculumAdapter:detect(raw)(the schema guard) andparse(raw)→CurriculumModel, plus a factory returning aSubjectCurriculum(listUnits/slice/progression/requiredCoverage/scopeValues). Export it fromsrc/curriculum/index.ts.
- Same graph shape as an existing subject (e.g. the CI-maths
-
Write the profile in
src/profiles/<subject>.ts— abuildProfile(grade, subject): SubjectProfileproviding:curriculum— from your adapter;deliverables— oneDeliverableSpecper document kind (key,label,scopeKind,classify(filename),dependsOn,promptFile);capabilities— feature flags (e.g.exampleDomainRotation,characterConsistency);buildGenerationContext(scope, deliverableKey)— the pre-generation payload for that subject.
-
Register it in
src/profiles/index.tsunder the"<grade>/<subject>"key. -
Build and select it:
npm run build, thenset_context("<grade>", "<subject>"). The guard runs your adapter'sdetect()against the KG; on a mismatch it refuses to activate and says why — nothing is silently mis-parsed.
Rules the build enforces: imports point down the layers above; service modules (storage/curriculum/generation) must not import profiles — pass what they need in as arguments (as reconcile(deliverables) and discoverDocuments(deliverables) do); cross-module imports go through the module's index.ts. npm run check:cycles fails the build on any import cycle.
A worked example is pending for CE1 reading (scope: one teacher guide per week); see
docs/multi-subject-architecture.md§9/§11 for the remaining steps.
Testing note
The storage layer sits behind a small StorageAdapter interface. The reconcile / history / variety / ingest logic is verified against an in-memory fake (no credentials needed). Unit tests run with npm test (Vitest); the example-domain neighborhood/suggestion logic is covered in src/generation/domains.test.ts. npm run build runs the import-cycle check (npm run check:cycles) before tsc, so a broken layer boundary fails the build. The Firebase implementation is compile-checked but not live-tested here — validating real bucket calls (list, signed URL, download, history read/write) needs your service-account credentials and network access, so do a first run against your own project.
Assumptions still baked in (tell me to change any)
- One grade/subject is active at a time; switching drops the KG, terminology, and history caches so the next call reloads for the new context.
- Deliverable classification is per-subject (a profile's
DeliverableSpec.classify). For CI maths: within a chapter subfolder, anything not "Fiches de leçons …" is the pupil manual. - Glossary derives from the KG, with the FR/Wolof file as fallback; characters are derived from what you log/ingest.
- "Latest" among duplicates is the object whose md5 matches history, else the most recently updated.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。