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:
@@ -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, 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 -->
|
||||
Reference in New Issue
Block a user