# 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, 1–2 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: ` 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=` - Machine-readable headers: - `SEARCH_ID=` - `FILTER_ID=` (only when a filter was created) - `META=` (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=` - 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=` / `SEARCH_ID=` 7. Optionally call `filter_search_results(search_id, ...)` - Returns `FILTER_ID=` 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=`, 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 user’s `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=` 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)