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.
639 lines
28 KiB
Markdown
639 lines
28 KiB
Markdown
# 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: <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 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=<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 -->
|