Germany Intelligence MCP
by mohitakki
README.md
<div align="center">
# π©πͺ Germany Intelligence MCP
**An evidence-based career intelligence layer for the German software job market, exposed to Claude as an MCP server.**
[](https://github.com/mohitakki/germany-intelligence-mcp/actions/workflows/ci.yml)
[](.nvmrc)
[](tsconfig.json)
[](tests/)
[](LICENSE)
[-orange)](docs/KNOWN-ISSUES.md)
*Not a job search tool. A decision engine that answers one question every morning:*
> ### βWhat should I learn today to maximise my chances of getting a software engineering job in Germany?β
</div>
---
## β οΈ Read this before using it
**v1.0 Core is infrastructure, not a finished product.** Its own QA pass graded it
**C β Needs Another Stabilization Sprint**, and that assessment is published in full:
| | |
|---|---|
| β
**Works** | Ingestion, deduplication, storage, trend analysis, full-text search, metrics, health, MCP tool surface |
| β οΈ **Known-wrong** | German language detection has **50% recall**. Extraction misses hyphenated German compounds in **24%** of postings |
| β **Not implemented** | 4 of 5 recommendation surfaces are stubs. The 5th would tell a C1 English speaker to *learn English* |
π **[docs/KNOWN-ISSUES.md](docs/KNOWN-ISSUES.md)** β every defect, measured, reproducible, and pinned by a test.
This honesty is deliberate. The project's core promise is that no claim is made
without evidence, and that has to apply to claims about the project itself.
---
## The idea
Most job tools optimise for *finding postings*. This one assumes finding jobs is easy
and **knowing what to learn is hard**. It treats the job market as a dataset and asks
what the evidence actually says.
```
ββββββββββββββββββββββββββββββββ
β Claude (reasoning layer) β
βββββββββββββββββ¬βββββββββββββββ
β MCP / JSON-RPC over stdio
βββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββ
β L5 MCP TOOL SURFACE 10 coarse tools Β· Zod-validated Β· evidence β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β L4 ENGINES trend Β· essentialism Β· gap Β· match Β· learning β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β L3 KNOWLEDGE STORE SQLite + FTS5 Β· repositories own all SQL β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β L2 ANALYSIS PIPELINE normalise β extract β classify β dedupe β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β L1 SOURCE ADAPTERS anti-corruption layer Β· one interface, N APIs β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
**Dependency rule:** arrows point downward only. An engine never imports a source
adapter; a source adapter never imports the database.
### What "evidence-based" means here
No number is emitted without its denominator. This is enforced by the **type system**
and the **database schema**, not by convention:
```ts
interface Evidence {
subject: string; // "ops.docker"
frequency: number; // 0.63
observedIn: number; // 214 β the numerator
totalAnalysed: number; // 339 β THE DENOMINATOR
windowDays: number; // 14
sampleJobIds: string[]; // openable, verifiable
meanConfidence: number; // how good was the extraction behind this
lowConfidence: boolean; // sample too small to act on
}
```
`recommendation.evidence` is `NOT NULL`. `pickTodayFocus()` cannot compile without
threading evidence through. A recommendation you cannot defend is unrepresentable.
---
## Quick start
```bash
git clone https://github.com/mohitakki/germany-intelligence-mcp.git
cd germany-intelligence-mcp
npm ci # no native build, no compiler
cp .env.example .env
cp config/profile.example.json config/profile.json # then edit your skill levels
npm test # 106 tests, ~40 against real SQLite
npm run ingest # live fetch from the German market
npm run health # is the data trustworthy right now?
```
**No native dependencies.** Storage is `node:sqlite` behind a driver port β see
[ADR-006](docs/ADR/006-storage.md) for why that decision and a production bug were
the same decision.
<details>
<summary><b>Wire it into Claude Desktop</b></summary>
```bash
npm run build
```
`claude_desktop_config.json`:
```json
{
"mcpServers": {
"germany-intelligence": {
"command": "node",
"args": ["--no-warnings", "/absolute/path/to/germany-intelligence-mcp/dist/index.js"],
"env": {
"GIM_DATA_DIR": "/absolute/path/to/germany-intelligence-mcp/data",
"GIM_PROFILE_PATH": "/absolute/path/to/germany-intelligence-mcp/config/profile.json"
}
}
}
}
```
Restart Claude, then ask: *"Run my daily briefing."*
</details>
---
## MCP tools
| Tool | Status | What it does |
|---|:--:|---|
| `health` | β
| Database + FTS, providers, last ingest age, extraction quality, jobs indexed. **Call this first if anything looks odd.** |
| `refresh_market` | β
| Fetch, normalise, dedupe, store. Returns full run metrics. |
| `get_market_trends` | β
| Technology demand with counts, frequencies, confidence and sample job ids. |
| `get_skill_gap` | β
| Your profile vs the market, bucketed and ranked, each item evidenced. |
| `search_jobs` | β
| BM25 full-text + filters. One row per deduplicated vacancy. |
| `list_sources` | β
| Providers, config state, legal status, compliance notes. |
| `mark_progress` | β
| Records learning so a skill stops being recommended. |
| `get_daily_briefing` | β | Market summary + trends + gaps. Recommendation sections are empty. |
| `get_today_focus` | β | Throws β Phase 6. |
| `generate_interview_questions` Β· `analyse_resume` | β | Return `[]` β Phase 7/8. |
Ten coarse tools, not thirty. Every description is loaded into Claude's context on
**every turn**, so tool sprawl directly degrades reasoning quality.
---
## Data sources
Only sources that permit programmatic access are shipped.
| Source | Status | Coverage |
|---|:--:|---|
| **Bundesagentur fΓΌr Arbeit** | β οΈ implemented, never network-tested | Germany's largest job database (~1M postings). Official federal API, public client id, no registration. |
| **Arbeitnow** | β
verified end-to-end | Public board API, no key. Berlin/Munich tech, English-language β the visa-sponsoring startup segment. |
| ATS boards (Greenhouse, Lever, Ashby, Personio, Recruitee) | π² Phase 3 | Canonical postings from the companies you actually want. Highest-value next build. |
| `src/sources/custom/` | π² slot | Your own adapters, disabled by default, `complianceNote` required. |
<details>
<summary><b>Why no LinkedIn / StepStone / Indeed / Xing</b></summary>
None offer an open job-search API. All four prohibit automated access in their Terms
of Service and enforce it technically. No adapter ships for them and none should be
added β an IP ban is the mild outcome; a legal notice while applying for a German
work visa is the bad one.
`src/sources/custom/` exists for sources you have a legitimate route to: company
career feeds, boards with a documented API, partner feeds, manual CSV import.
See [ADR-001](docs/ADR/001-provider.md).
</details>
---
## How a recommendation earns its place
Five gates, all of which must pass:
1. **Sample floor** β β₯ 20 deduplicated jobs analysed
2. **Evidence floor** β the skill appears in β₯ 5 postings
3. **Relevance floor** β β₯ 10% of jobs request it
4. **Deficit** β your self-assessed level is below 4/5
5. **Cooldown** β not recommended in the last 21 days
Survivors are ranked:
```
priority = marketFrequency Γ skillDeficit Γ momentum
```
Multiplicative, not additive β so a skill you already have scores β 0 no matter how
in-demand it is. That is the mechanism that stops the system telling you to learn
React for the fortieth time.
---
## The engineering story
This repo went through **four adversarial cycles** after "done". Each one is documented,
because the findings are more interesting than the code.
### π΄ The write path was dead and the run said `ok`
`JobRepository.upsert` ended with `INSERT INTO job_fts (...) ON CONFLICT DO NOTHING`.
SQLite rejects UPSERT against a virtual table. Every insert threw, `runIngest` caught
it per-job, and the run reported success having stored **nothing**.
The schema had been "verified" by running a hand-written query against hand-inserted
rows β never the repository's own statement. **A test that doesn't cross the boundary
tests nothing.** β [ADR-006](docs/ADR/006-storage.md)
### π΄ A Berlin train line was inflating AWS demand
Eight of twelve ordinary German sentences produced phantom skill demand:
| Sentence (verbatim from real postings) | Phantom skill |
|---|---|
| "no **less** than three years of experience" | CSS/Less |
| "the position starts in **spring** 2027" | Spring Boot |
| "gut erreichbar mit der **S3** und der U2" | AWS β *an S-Bahn line* |
| "you **react** quickly to incidents" | React |
The defect wasn't the aliases β it was the **default**. A token was assumed technical
unless something stopped it. Now ambiguous aliases are assumed non-technical unless a
requirement marker or a confirmed technology list proves otherwise.
β [ADR-003](docs/ADR/003-taxonomy.md)
### π΄ The dedup threshold was picked, not measured
Version one used Hamming β€ 3 because it "felt safely conservative." It matched nothing
but byte-identical text.
```
0 bits identical 11 bits ~15% extra boilerplate
5 bits hyphenation edit 13 bits bullets reordered
9 bits one extra sentence 31 bits a completely different job
```
Real variants cluster at 5β13; unrelated documents sit past 30. **12 sits in a wide,
empty gap.** β [ADR-002](docs/ADR/002-dedup.md)
### π΄ The extractor was accurate; the data was wrong
Ground truth over 111 real postings: **96.6% recall, 1 false positive in 59 mentions.**
The extractor is fine. The corpus genuinely contains almost no React/TypeScript work β
TypeScript appears in 3 of 111 postings.
Diagnosing this as a *source selection* problem rather than an extraction problem is
the difference between fixing it and tuning the wrong knob.
---
## Documentation
| Document | What it is |
|---|---|
| **[KNOWN-ISSUES.md](docs/KNOWN-ISSUES.md)** | Every measured defect with a reproduction. **Read before trusting output.** |
| **[RUNBOOK.md](docs/RUNBOOK.md)** | Daily operation, metric interpretation, backup, replay, troubleshooting |
| [ARCHITECTURE.md](ARCHITECTURE.md) | Layers, data flow, dependency rules, trade-offs |
| [ROADMAP.md](ROADMAP.md) | Build order with reasoning, ~35 h to complete |
| [docs/ADR/](docs/ADR/README.md) | 7 decision records β **why**, with rejected alternatives |
<details>
<summary><b>Architecture Decision Records</b></summary>
| ADR | Decision |
|---|---|
| [001](docs/ADR/001-provider.md) | Provider architecture and the anti-corruption layer |
| [002](docs/ADR/002-dedup.md) | SimHash deduplication, blocking, measured threshold |
| [003](docs/ADR/003-taxonomy.md) | Curated taxonomy + context-window gating |
| [004](docs/ADR/004-prompt-boundary.md) | Untrusted-content trust boundary |
| [005](docs/ADR/005-llm.md) | LLM as additive enrichment only, never authoritative |
| [006](docs/ADR/006-storage.md) | SQLite via `node:sqlite` behind a driver port |
| [007](docs/ADR/007-evidence.md) | Evidence as a required type |
</details>
---
## Testing
**106 tests.** ~40 cross into real SQLite. Three suites, three jobs:
```bash
npm test # everything
npx tsx --test tests/regressions.test.ts # one block per shipped bug
npx tsx --test tests/corpus.test.ts # 111 real postings, pinned baselines
```
- **`extraction.test.ts`** β pure logic: German gender markers, company suffixes, SimHash, the essentialism filter
- **`regressions.test.ts`** β one `describe` per critical bug, with the original defect in the comment so nobody "fixes" the test later
- **`ingest.test.ts`** β pipeline β repository β SQLite, error isolation, migration idempotency
- **`corpus.test.ts`** β 111 real German postings. Pins the measured baselines **and the known defects**. When you fix one, its test fails β that failure is the proof.
The corpus is [committed](tests/fixtures/corpus-de-111.json) so every number in the
docs is reproducible.
---
## Security
- **Prompt injection** β job text is attacker-controlled. Nonce-delimited envelope built *before* the LLM path exists; detection counts, never filters ([ADR-004](docs/ADR/004-prompt-boundary.md))
- **Credential redaction** β no query string ever reaches a log line, enforced at the boundary and again centrally
- **SQL injection** β fully parameterised; verified against `'); DROP TABLE job;--`
- **Adversarial robustness** β 27 hostile inputs (500 KB descriptions, malformed HTML, emoji, RTL, null bytes, FTS syntax), **zero crashes**
---
## Contributing
Personal project, but the patterns are reusable. If you fork it:
1. Read [ADR-003](docs/ADR/003-taxonomy.md) before touching the taxonomy β several constraints look arbitrary and are load-bearing
2. `npm test` must stay green; the false-positive corpus is the guard rail
3. New decisions get a new ADR β [template](docs/ADR/000-template.md). ADRs are immutable once Accepted; supersede, don't edit
---
<div align="center">
**MIT** Β· Built as a systems-engineering exercise in evidence-based recommendation.
Not affiliated with Bundesagentur fΓΌr Arbeit or Arbeitnow.
</div>
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues