Files
Orchestrator/bahn/teamlandkarte-mcp/README.md
T
ankn a5f8fb49ab Migrate all repos into monorepo context folders
Bahn: aisupport, Analyse-O2C-C2S, awesome-bahn-mcp-servers, beam-mcp,
      Confluence_Bot, db-planet-mcp-server, O2C-Harness, project-audit,
      Projekt-KIQ-HP, teamlandkarte-mcp
Dhive: Jury-Voting
Privat: CV, NoteGraph (NOTE: NoteGraph needs complete redo after consolidation)
Shared: AI-Orchestrator, OrgMyLife, power_skills_and_more
Shared/references: symphony (read-only)

Bahn repos remain available as independent remotes - this monorepo
pulls them in via subtree, the originals are untouched.
2026-06-30 20:39:52 +02:00

639 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Teamlandkarte MCP Server
An MCP (Model Context Protocol) server that enables AI assistants to match DB Systel employees with free work capacity to task requirements.
Key matching semantics (refined):
- Role inference is **title-first** with description fallback.
- Role and competence inference use **separate cached embeddings**.
- Scores are shown as **Role Score**, **Competence Score**, and **Overall Score**.
- Result categories include **Top / Good / Partial / Low / Irrelevant**.
- The Low vs Irrelevant boundary is configurable via `matching.thresholds.low`.
- Availability in matching outputs is shown as an **overlap percentage** with the reference time range.
## Features
- Dual input modes: free-text task description or structured parameters
- Matching and scoring: competences (default 0.8) + role (default 0.2)
- Interactive exploration: view Top results first, then explore other categories via pagination
- Smart caching: DB cache (12h TTL) and search cache (60min TTL)
- Read-only operations: safe, non-invasive database access
- Embedding preload on startup (cache misses only):
- role vocabulary + competence vocabulary from the Data Lake
- all open tasks (embedded by `task_id`)
- Stable, table-first outputs designed for MCP chat UIs:
- Task/validation/role inference tools return Markdown tables first
- Search tools emit deterministic headers for robust `search_id` parsing
### Azure OpenAI usage
This server uses **Azure OpenAI** for:
- Role inference (task text → closest role vocabulary entry) — **embeddings**
- Competence inference (task text → closest competence vocabulary entries) — **embeddings**
- Capacity↔task matching similarity (default: **embeddings**; optional: **BM25+RRF**)
- Auto-Tagging competence expansion (optional: **chat/LLM**, requires `use_auto_tagging = true`)
The chat/LLM path is disabled by default. Enable it via `use_auto_tagging = true` in
`config.toml` (requires `AZURE_OPENAI_LLM_API_KEY` and `azure_openai.chat_deployment`).
## Quick Start
### Prerequisites
- Python 3.13+
- Access to DB Systel Open Data Lake (Trino/Presto)
- Valid database credentials
- Azure OpenAI endpoint + **embedding** API key (via environment variables)
### Installation
1. Clone the repository:
```bash
git clone https://git.tech.rz.db.de/ThomasHandke/teamlandkarte-mcp.git
cd teamlandkarte-mcp
```
2. Install dependencies:
```bash
uv sync
# or
pip install -e .
```
3. Configure the server:
```bash
cp config.toml.example config.toml
# Edit config.toml with host/port, cache settings, and Azure OpenAI endpoint/deployment
# Create a .env file with credentials (do not commit):
# DATA_LAKE_USERNAME="..."
# DATA_LAKE_PASSWORD="..."
# AZURE_OPENAI_EMBEDDING_API_KEY="..."
# AZURE_OPENAI_LLM_API_KEY="..." # only needed when use_auto_tagging = true
```
### Running the Server
Run the MCP server locally via stdio:
From an activated project environment:
- `uv run teamlandkarte-mcp --config config.toml --log-level INFO`
If your MCP host (e.g. Cherry Studio) cannot set a working directory, wrap it
so the command runs in the project folder:
- `zsh -lc 'cd /Users/thomashandke/ws/teamlandkarte-mcp && uv run python -m teamlandkarte_mcp --config /Users/thomashandke/ws/teamlandkarte-mcp/config.toml --log-level DEBUG'`
Important:
- When using stdio MCP, **stdout must be reserved for the JSON-RPC protocol**.
Logs go to **stderr**.
## Running via Cherry Studio with `uvx` (Hot Reload)
If you develop locally and want **hot reload** without relying on a project-local virtualenv being activated in Cherry Studio, you can start the server through `uvx` and `watchfiles`.
This setup:
- restarts the MCP server process on `*.py` changes
- watches only `src/teamlandkarte_mcp/`
- works with the SQLite embedding cache (`[embedding_cache].path`) because the cache is a file on disk and will be reused after restarts
Cherry Studio command (macOS / zsh):
- **Command**: `zsh`
- **Args**:
- `-lc`
- `cd /Users/thomashandke/ws/teamlandkarte-mcp && uvx --from watchfiles watchmedo auto-restart --recursive --pattern '*.py' --directory src/teamlandkarte_mcp -- uv run python -m teamlandkarte_mcp --config /Users/thomashandke/ws/teamlandkarte-mcp/config.toml --log-level DEBUG`
Notes:
- The embedding cache DB file (for example `.cache/embeddings.sqlite3`) survives restarts; deleting it is safe but will increase Azure embedding calls until it warms up again.
- If you see occasional `database is locked` errors during rapid file changes, it usually means two processes overlapped briefly. In that case, reduce the frequency of changes saved at once or adjust the watcher settings.
## Configuration
Edit `config.toml` to customize:
- Trino connection settings (host/port, TLS, catalog/schema)
- Connection management: `pool_size`
- Matching weights and thresholds (Round 6 semantics)
- Cache TTLs and sizes
- Confirmation gate (`matching.require_confirmation`)
- Fuzzy filtering threshold (`matching.fuzzy.min_similarity`)
- Competence inference knobs (`matching.inference.*`):
- `max_competences` (int | null)
- `min_similarity` (float | null)
- Azure OpenAI settings (`[azure_openai]`) — embeddings + optional chat model for Auto-Tagging
- Embedding cache settings (`[embedding_cache]`)
- BM25 + RRF lexical matching flags (`[matching.similarity]`):
- `use_bm25_search` (boolean, default `false`): enables BM25+RRF lexical competence matching
instead of embedding-based similarity. Skills with no token overlap score exactly 0.0,
eliminating embedding-based false positives.
- `use_auto_tagging` (boolean, default `false`): enables LLM pre-expansion of each
candidate's competence list before BM25 scoring. Bridges synonym/abbreviation/cross-language
gaps (e.g. "ML" → "Machine Learning"). Only active when `use_bm25_search = true`.
- Chat model deployment for Auto-Tagging (`azure_openai.chat_deployment`, default `""`):
Azure deployment name for the chat model (e.g. `"gpt-4.1"`). Required when
`use_auto_tagging = true`.
Database credentials are read from environment variables (recommended: a local
`.env` file):
- `DATA_LAKE_USERNAME`
- `DATA_LAKE_PASSWORD`
Azure OpenAI credentials are read from environment variables:
- `AZURE_OPENAI_EMBEDDING_API_KEY` — API key for embedding model (always required)
- `AZURE_OPENAI_LLM_API_KEY` — API key for chat model used by Auto-Tagging
(required only when `use_auto_tagging = true`)
See `config.toml.example` for a complete template.
For a step-by-step guide to configuring Azure deployments and keys, see:
- `docs/azure_openai_setup.md`
## Usage
### Profile types: `capacity` vs `team` search
The server supports two **profile types** for task matching, selected via the tool name:
| `Profile_Type` | Entry point | What is matched |
|----------------|-------------|-----------------|
| `capacity` (existing) | `find_matching_capacities(...)` | The task is matched against employee capacity profiles. Availability filters apply. |
| `team` (new) | `find_matching_teams(...)` | The task is matched against aggregated team profiles (`Team_Name`, `Schwerpunkt`, `Über uns`, `Leistungen`, `Interessen`, Top/Non-Top competences, references). Availability filters do **not** apply. |
The persisted search session is tagged accordingly:
- Capacity searches: `search_type = "capacity_search"` in `SearchCache` and the META JSON.
- Team searches: `search_type = "team_search"` in `SearchCache` and the META JSON.
`find_matching_teams` accepts the same `matching_method` parameter (`score` | `llm_fulltext`) as `find_matching_capacities`. In `score` mode the team's `focus_name` is used as the role stand-in, and Top competences are upweighted by the configurable factor `matching.team.top_competency_weight` (default `1.5`, must be numeric and `>= 1.0`).
Result columns differ between the two profile types:
| Mode | Columns (capacity) | Columns (team) |
|------|--------------------|----------------|
| `score` | `Owner`, `Role`, `Competences`, `Availability`, `Role Score`, `Competence Score`, `Overall Score`, `Category` | `Team Name`, `Schwerpunkt`, `Top-Kompetenzen`, `Role Score`, `Competence Score`, `Overall Score`, `Category` |
| `llm_fulltext` | `Owner`, `Role`, `Competences`, `Availability`, `Category`, `Begründung` | `Team Name`, `Schwerpunkt`, `Top-Kompetenzen`, `Category`, `Begründung` |
Filter behavior on a `team_search` cache entry:
- `role_filter` matches against `team.focus_name` (Schwerpunkt).
- `competence_filter` matches against the team's competence list. Filter values with the suffix `(Top)` are restricted to Top competences (the suffix is stripped before comparison).
- `availability_date_start`, `availability_date_end`, and `is_fully_available` are accepted but **ignored**. Each value is surfaced in the `Applied Filters` table with a `team_search` not-effective hint.
### Matching Methods
The server supports **two matching methods** that callers select per call via the `matching_method` parameter on `find_matching_capacities`, `find_matching_tasks`, and `find_matching_teams`:
| Value | Description | Output columns |
|-------|-------------|----------------|
| `score` (default) | BM25+RRF competence similarity + LLM role similarity. Existing behavior. | `Role Score`, `Competence Score`, `Overall Score`, `Category` |
| `llm_fulltext` | LLM-based full-text comparison of complete profiles via Azure OpenAI Chat Completion. One call per candidate, returns `{category, rationale}`. | `Category`, `Begründung` (rationale, 12 sentences). **No numeric scores.** |
When `matching_method` is omitted, the server uses the configured default:
```toml
[matching]
# Default method for new searches when callers omit matching_method.
# Allowed: "score" | "llm_fulltext"
default_method = "score"
```
Both methods share the same prefilters (e.g. availability), the same `search_id`/`filter_id` flow, and the same Markdown summary table. Differences:
- In `llm_fulltext` mode the result tables use a `Begründung` column instead of the score columns. The persisted META JSON contains `"matching_method": "llm_fulltext"` so `get_results_by_category` and `filter_search_results` render the correct schema automatically.
- In `llm_fulltext` mode `filter_search_results` ignores `min_similarity` (it is not applicable without numeric scores). It is surfaced in the Applied Filters table as a hint.
#### Additional database views in `llm_fulltext` mode
In addition to the views used by the `score` method, `llm_fulltext` reads:
- `teamlandkarte_v_capacities_latest.description`
- `teamlandkarte_v_capacity_certificates_latest` (joined on `capacity_id`)
- `teamlandkarte_v_capacity_references_latest` (joined on `capacity_id`)
- `teamlandkarte_v_partners_latest` — joined to references via a **LEFT JOIN** `teamlandkarte_v_capacity_references_latest.partner_id = teamlandkarte_v_partners_latest.id`. The partner `name` column is exposed as `partner_name` and surfaces inside the LLM-visible profile. When `partner_id` is `NULL` or no partner matches, `partner_name` is the empty string and the reference is still kept (only the `Partner:` token is omitted in the serialized profile).
The partner LEFT JOIN is part of the same SQL query that loads references; no extra round-trip is needed.
#### Example output (LLM mode)
A capacity-search result table in `llm_fulltext` mode looks roughly like this:
```markdown
## Summary
| Category | Count |
| --- | --- |
| Top | 1 |
| Good | 2 |
| Partial | 0 |
| Low | 0 |
| Irrelevant | 1 |
## Top
| ID | Owner | Role | Competences | Availability | Category | Begründung |
| --- | --- | --- | --- | --- | --- | --- |
| 12345 | Erika Musterfrau | Senior Backend Developer | Python, FastAPI, PostgreSQL | 100% | Top | Profil deckt Python/FastAPI vollständig ab und enthält Referenzprojekte mit identischem Tech-Stack. |
```
A typical META line emitted by the matching tools (and echoed by `get_results_by_category` / `filter_search_results`):
```text
META={"search_id":"7c0e6f50-...","filter_id":null,"default_category":"Top","matching_method":"llm_fulltext"}
```
In `score` mode the same META line carries `"matching_method":"score"` and the result table keeps the existing `Role Score | Competence Score | Overall Score | Category` columns.
#### Mini walkthrough (LLM mode)
A typical agent-driven flow when the user asks for an LLM-based capacity search:
1. Capture and confirm requirements as usual (`collect_structured_requirement_data`, `show_pending_requirements`, `confirm_requirements(confirm=true)`).
2. Ask the user once: *"Welches Verfahren soll ich verwenden `score` oder `llm_fulltext`?"*
3. Call the tool with the chosen method:
```python
find_matching_capacities(
role_name="Senior Backend Developer",
competences=["Python", "FastAPI", "PostgreSQL"],
date_start="2025-03-01",
date_end="2025-12-31",
matching_method="llm_fulltext",
)
```
4. The tool returns the `Summary` counts + the first non-empty category table (with the `Begründung` column) and emits a META line containing `"matching_method":"llm_fulltext"`.
5. Page through other categories with `get_results_by_category(search_id=..., category="Good", ...)`. Because the persisted payload carries `matching_method`, the table is rendered automatically with `Begründung` instead of score columns.
6. Optional refinement via `filter_search_results(search_id=..., role_filter=..., competence_filter=...)`. `min_similarity` is accepted but ignored in this mode and surfaces in Applied Filters as `min_similarity (ignored: not applicable in llm_fulltext mode)`.
### Capacity browsing + capacity→task matching
New tools for capacity-driven workflows:
- `list_free_capacities(limit=20)`
- `get_capacity_details(capacity_id)`
- `find_matching_tasks(capacity_id)`
### Team browsing + team matching
Tools for team-driven workflows:
- `list_teams(limit=20)` — Markdown table with columns `Team Id`, `Team Name`, `Schwerpunkt`, `Anzahl Kompetenzen`, `Anzahl Referenzen`.
- `get_team_details(team_id)` — Markdown table for one team plus sections `## Über uns`, `## Leistungen`, `## Interessen`, `## Kompetenzen` (with `(Top)` marker), `## Referenzen` (Partner_Name + Projekte) and `## Next steps`. Returns `Team not found: <team_id>` for unknown ids.
- `find_matching_teams(role_name, competences, matching_method?)` — Match a task against team profiles. No date parameters: availability filters are not applicable to team searches.
### Task-driven matching (task→capacity)
Core tools:
- `list_open_tasks(limit=...)`
- `get_task_details(task_id)`
- `validate_task_requirements(task_id)` (embeddings-only inference tables)
- `find_matching_capacities(role_name, competences, date_start?, date_end?)`
### Note on dates
With chat removed, dates are **not extracted** from free text.
Provide dates via `collect_structured_requirement_data(...)` / guided capture.
### Tool workflow overview
This server exposes multiple MCP tools for three main activities:
1. **DB task workflow** (browse → inspect → validate → match)
2. **Ad-hoc requirement gathering** (extract/update/structured/guided)
3. **Search execution + refinement** (match → filter → page by category)
Important UX notes:
- Scoring is **categorical-first** (`Top`/`Good`/`Partial`/`Low`). Numeric scores
remain visible for transparency.
- Matching can be **hard-gated by confirmation** (default). Requirement capture
tools write *pending* requirements which must be confirmed before matching.
### Confirmation gating
By default, matching is blocked until the user confirms the pending requirements:
> Note: The server tracks pending/confirmed requirements in in-memory session
> state. This is a convenience guard for typical single-client chats and is not
> a multi-tenant source of truth. Clients/agents must enforce the confirmation
> rule themselves.
- Capture requirements using one of:
- `extract_requirements(...)`
- `collect_structured_requirement_data(...)` (optionally with `description=...`)
- guided capture tools (see below)
- `update_requirements(...)`
If `matching.require_confirmation = true`:
- Review + ask user for confirmation (**must be after the last requirements update**):
- `show_pending_requirements()`
- ask the user to confirm (Yes/No)
- Confirm them (only after explicit user approval):
- `confirm_requirements(confirm=true)`
- Then run:
- `find_matching_capacities(...)`
If `matching.require_confirmation = false`:
- Do **not** ask for confirmation.
- Do **not** call `show_pending_requirements()` / `confirm_requirements()`.
- Proceed directly to matching once requirements are complete.
You can disable the gate for convenience/testing via:
- `matching.require_confirmation = false` in `config.toml`
### Guided capture (step-by-step)
If the user did not provide enough project details, use the guided tools:
1. `start_guided_capture()`
2. `guided_set_description(description)`
3. `guided_set_role(role_name)`
4. `guided_set_time_range(date_start?, date_end?)` (open-ended allowed)
5. `guided_set_competences(competences)` (writes pending requirements)
The assistant MUST ensure a **concrete topic/goal description** exists.
A single skill request like "JavaScript" is not sufficient; ask for scope/goal.
6. Review + ask user:
- `show_pending_requirements()` (preferred)
- ask the user to confirm (Yes/No)
7. Confirm (only after user approval):
- `confirm_requirements(confirm=true)`
8. `find_matching_capacities(...)`
### Search sessions: `search_id` / `filter_id` (robust parsing)
Search-related tools (`find_matching_capacities`, `filter_search_results`, `get_results_by_category`) emit deterministic, machine-parseable headers.
**Always treat the *most recent* tool output as authoritative**.
- First line marker (required by some clients):
- `Using SEARCH_ID=<uuid>`
- Machine-readable headers:
- `SEARCH_ID=<uuid>`
- `FILTER_ID=<uuid>` (only when a filter was created)
- `META=<json>` (includes status and echo of ids)
Output shape notes:
- `find_matching_capacities` includes a `## Summary` counts table (Top/Good/Partial/Low/Irrelevant) and a results table for the **first non-empty category**.
- Matching and pagination tables show:
- Availability as **overlap %**
- Separate **Role Score / Competence Score / Overall Score** columns
- No `Level` column in matching result tables (Level is only shown in capacity listing tools).
- `filter_search_results` supports both search directions:
- capacity search filtering (role/competences/availability)
- task search filtering (task text and inferred required competences)
- optional full-coverage availability (`is_fully_available=true`)
- `filter_search_results` does **not** repeat the summary counts. It returns:
- `FILTER_ID=<uuid>`
- an **Applied Filters** table
- a flat results table (top rows) with **separate** columns for `Score` and `Category`
- the results table is always a Markdown table (even for a single matching row, and also when there are zero matches)
- `min_similarity` is only applied when the user provides `role_filter` and/or `competence_filter` (availability-only filtering does not apply similarity).
- `get_results_by_category` does **not** repeat the summary counts. It returns a single category page table.
Notes:
- There is intentionally **no** tool that returns a "last search id" (multi-user/session unsafe). Always extract the id from tool output.
- Pagination/filter tools reject non-UUID inputs early.
- If a cache entry is missing/expired, the tools return a table-first error payload with `META.status=unknown_or_expired`.
### Typical workflow (DB task)
1. Call `list_open_tasks(limit=...)`
2. Call `get_task_details(task_id)`
3. (Optional, only on explicit user request) Call `validate_task_requirements(task_id)`
- Validation is independent from matching and is **not** required to search.
4. Capture/update requirements (e.g. via `collect_structured_requirement_data(...)`)
5. (If confirmation is enabled) review + confirm requirements (**after the last update**):
- `show_pending_requirements()`
- ask the user to confirm (Yes/No)
- `confirm_requirements(confirm=true)`
6. Call `find_capacities_for_task(task_id)` or `find_matching_capacities(...)`
- Returns headers including `Using SEARCH_ID=<uuid>` / `SEARCH_ID=<uuid>`
7. Optionally call `filter_search_results(search_id, ...)`
- Returns `FILTER_ID=<uuid>` under the same `SEARCH_ID`
8. Page through results using `get_results_by_category(search_id, filter_id?, category, page, page_size)`
If you see `unknown_or_expired` or `invalid_search_id_format`, re-run the matching tool to create a new search.
Notes:
- Round 6 semantics: scoring = competence (0.8) + role (0.2) only
- Availability is displayed and can be used as an optional date-range filter only
- Open-ended capacity end dates (`end_date = null`) are treated as "available without limit" for filtering
## Global rules
1. **Always prefer the latest search**
- When a tool returns a JSON block containing `search_id`, treat it as the current/latest search.
- Replace any previously stored search context with this latest `search_id`.
- If the user says “show Good/Partial/Low” without a `search_id`, use the latest `search_id`.
- If the tool output contains a line `Using SEARCH_ID=<uuid>`, treat **that exact value** as authoritative.
- Never guess or substitute a UUID from other context. Only use search IDs that were returned by tools.
- **Never “make up” a UUID** and never copy a UUID from user prose/logs—only from tool output headers.
2. **Do not rerun a search unless necessary**
- If the user asks to show another category/page for an existing search, do not call `find_matching_capacities` again.
- Only rerun matching if:
- no prior `search_id` exists, or
- the user explicitly requests a new search, or
- `get_results_by_category` / `filter_search_results` returns “unknown_or_expired”. In that case: **do not try another ID**; rerun `find_matching_capacities`.
3. **If the users `search_id` is invalid**
- If the server returns “Invalid search_id format (expected UUID)”, instruct the user to copy the UUID from the tool output header (`SEARCH_ID=<uuid>` or `META={...}`).
4. **Confirmation gate**
- If `find_matching_capacities` returns a confirmation error (requirements must be confirmed), do **not** auto-confirm.
- Call `show_pending_requirements()` (preferred) to display the review table.
- Ask the user explicitly to confirm (Yes/No).
- Only after user approval call `confirm_requirements(confirm=true)`.
- Then rerun the matching call.
- If the user says No: call `confirm_requirements(confirm=false)` and continue requirement capture.
- (Alternative marker: if requirements were already shown to the user in chat, call `request_requirements_confirmation()` before `confirm_requirements(...)`.)
- If confirmation is disabled by config, proceed directly.
5. **Be table-first**
- Prefer tool outputs that are Markdown tables.
## Tools and when to use them
## Database Schema
The server queries these views from the Open Data Lake (Trino):
Capacity profiles:
- `teamlandkarte_v_capacities_latest`
- `teamlandkarte_v_capacity_competences_latest`
- `teamlandkarte_v_competences_latest`
Capacity LLM-fulltext profiles additionally read:
- `teamlandkarte_v_capacities_latest.description`
- `teamlandkarte_v_capacity_certificates_latest` (joined on `capacity_id`)
- `teamlandkarte_v_capacity_references_latest` (joined on `capacity_id`)
- `teamlandkarte_v_partners_latest` (LEFT JOIN on `capacity_references.partner_id = partners.id`)
Team profiles read four additional views (used by `find_matching_teams`, `list_teams`, and `get_team_details`):
- `teamlandkarte_v_teams_latest` — team master data (`team_id`, `ouid`, `about_us`, `offerings`, `interests`, `focus_name`).
- `teamlandkarte_v_teammeter_organizational_units_latest` — joined to teams via **INNER JOIN** `teams_latest.team_id = organizational_units_latest.id`. The column `name` is exposed as the team name. Teams without a matching organizational unit row are excluded by the inner join.
- `teamlandkarte_v_teammeter_team_competences_latest` — joined to teams via `ouid`. Each entry carries a `top_competency` flag (NULL is normalized to `false`). The competence name is resolved via `teamlandkarte_v_competences_latest` analogous to capacity competences. Entries without a resolvable competence name are filtered out.
- `teamlandkarte_v_team_references_latest` — joined to teams via `ouid`, with a **LEFT JOIN** `team_references.partner_id = partners_latest.id` to resolve the partner name. References with `NULL` or whitespace-only `projects` are filtered out; references without a matching partner keep `partner_name = ""` and are still returned.
The partner LEFT JOIN is part of the same SQL query that loads the references, so no extra round-trip is needed.
Tasks/skills are read from:
- `beschaffungstool_kmp_task_latest`
- `beschaffungstool_kmp_skill_latest`
Notes:
- The task "required role name" field is currently **not available** in the DB
and is therefore not shown.
## Embeddings performance: global prefetch + batching
Matching uses Azure OpenAI embeddings for competence/role similarity.
To reduce latency and cost, the server:
- deduplicates and **prefetches embeddings globally per matching run** (required +
all candidate competences/roles)
- resolves embeddings via in-memory cache → SQLite cache → Azure API
- uses **batched** Azure embeddings calls (`input=[...]`) with sequential
chunking
### Tuning batch size
You can adjust the number of inputs per Azure embeddings request via:
- `azure_openai.embedding_batch_size` (default: `128`)
See `config.toml.example` for details.
## Development
### Project Structure
```
teamlandkarte-mcp/
├── src/
│ └── teamlandkarte_mcp/
│ ├── mcp_server.py # MCP server implementation
│ ├── config.py # Configuration management
│ ├── database/
│ │ └── trino_client.py # Trino database client
│ ├── matching/
│ │ ├── matcher.py # Matching algorithm
│ │ └── scorer.py # Scoring logic
│ └── cache/
│ └── query_cache.py # Result caching
├── openspec/ # Specifications & changes
├── config.toml # Configuration (not in VCS)
└── main.py # Entry point
```
### OpenSpec Workflow
This project uses OpenSpec for spec-driven development:
```bash
# View active changes
openspec list
# View current change
openspec show add-capacity-matching-mcp-server
# Validate specifications
openspec validate --strict
```
## Local Setup (uv)
This project uses `uv` to manage dependencies and a virtual environment under `.venv`.
1. Create/sync the environment:
- `uv venv .venv`
- `uv pip sync pyproject.toml`
2. Install the project as editable (creates the `teamlandkarte-mcp` console script):
- `uv pip install -e .`
3. Configure credentials:
- Copy `config.toml.example` to `config.toml` and fill in your credentials.
- `config.toml` is ignored via `.gitignore` and must stay local-only.
## Running (stdio MCP)
Run the MCP server locally via stdio:
- `.venv/bin/teamlandkarte-mcp`
If your shell does not automatically find the console script, always use the explicit path above.
## Security
- `config.toml` contains credentials and must not be committed.
- If credentials were pasted into chat logs or ever committed to git history, rotate them.
See `docs/security.md`.
## Documentation
- Configuration reference: `config.toml.example`
- Architecture and ADRs: `openspec/changes/add-capacity-matching-mcp-server/architecture.md`
- Design notes: `openspec/changes/add-capacity-matching-mcp-server/design.md`
- Capacity matching specification (Round 6): `openspec/changes/add-capacity-matching-mcp-server/specs/capacity-matching/spec.md`
- Security: `docs/security.md`
## Roadmap
**Stage 1** (Current): Basic capacity matching
- Free-text and structured input modes
- LLM-based competence matching
- Categorical scoring
- Result caching
**Future Stages**:
- Advanced filtering options
- Capacity booking workflow
- Historical trend analysis
- Integration with other HR systems
## Contributing
1. Review the [OpenSpec workflow](openspec/AGENTS.md)
2. Create change proposals for significant features
3. Follow the implementation tasks
4. Submit merge requests on GitLab
## License
[Add license information]
## Authors
Thomas Handke - Deutsche Bahn Systel GmbH
## Links
- [GitLab Repository](https://git.tech.rz.db.de/ThomasHandke/teamlandkarte-mcp)
- [Model Context Protocol](https://modelcontextprotocol.io/)
- [DB Systel Open Data Lake Documentation](https://docs.example.com) <!-- Update with actual link -->