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.
This commit is contained in:
2026-06-30 20:39:52 +02:00
parent 2f2b295531
commit a5f8fb49ab
1717 changed files with 447332 additions and 0 deletions
+638
View File
@@ -0,0 +1,638 @@
# 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 -->