chore(bahn): remove beam-mcp (educational only, not actively used)
This commit is contained in:
@@ -1,25 +0,0 @@
|
|||||||
# BEAM MCP Server - Konfiguration
|
|
||||||
# Kopiere diese Datei nach .env und passe die Werte an (optional)
|
|
||||||
|
|
||||||
# ── BEAM URLs (optional, Standardwerte sind vorbelegt) ───────────────────────
|
|
||||||
# Basis-URL der BEAM Instanz
|
|
||||||
BEAM_BASE_URL=https://db.leanix.net
|
|
||||||
|
|
||||||
# Login-URL (für automatischen Browser-Login)
|
|
||||||
BEAM_LOGIN_URL=http://db.de/beam
|
|
||||||
|
|
||||||
# Workspace-Name (für Token-Key im localStorage)
|
|
||||||
BEAM_WORKSPACE=beam
|
|
||||||
|
|
||||||
# ── Browser-Login (optional) ─────────────────────────────────────────────────
|
|
||||||
# Headless-Modus für automatischen Browser-Login (true/false)
|
|
||||||
# Bei Anmeldeproblemen auf false setzen, um den Browser sichtbar zu starten
|
|
||||||
BEAM_HEADLESS=true
|
|
||||||
|
|
||||||
# ── Authentifizierung ────────────────────────────────────────────────────────
|
|
||||||
# Das BEAM-Token wird automatisch im OS-Keychain gespeichert:
|
|
||||||
# - macOS: Keychain Access (via `security` CLI)
|
|
||||||
# - Windows: DPAPI-verschlüsselte Datei in %LOCALAPPDATA%/beam-mcp/
|
|
||||||
#
|
|
||||||
# Erstmaliger Login: `npm run login` oder automatisch beim ersten API-Aufruf.
|
|
||||||
# Danach wird das Token aus dem Keychain geladen — kein erneuter Login nötig.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
package-lock.json
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
.idea
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
---
|
|
||||||
inclusion: manual
|
|
||||||
---
|
|
||||||
|
|
||||||
# Neues BEAM Tool hinzufügen
|
|
||||||
|
|
||||||
Schritt-für-Schritt Anleitung zum Erweitern des beam-mcp-servers um ein neues Tool.
|
|
||||||
|
|
||||||
## 1. API-Funktion in `src/api.ts`
|
|
||||||
|
|
||||||
Neue Datenabfragen gehören in `api.ts`. Nutze die bestehende `gql()`-Funktion für GraphQL-Queries:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export async function myNewFunction(param: string) {
|
|
||||||
// UUID-Erkennung: wenn keine UUID, per Suche auflösen
|
|
||||||
let uuid = param;
|
|
||||||
if (!param.match(/^[0-9a-f-]{36}$/i)) {
|
|
||||||
const results = await searchFactSheets(param);
|
|
||||||
if (!results.length) throw new Error(`No factsheet found for: ${param}`);
|
|
||||||
uuid = results[0].id as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await gql(
|
|
||||||
`query MyQuery($id: ID!) {
|
|
||||||
factSheet(id: $id) {
|
|
||||||
id name type
|
|
||||||
... on Application {
|
|
||||||
// gewünschte Felder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
{ id: uuid }
|
|
||||||
)) as { factSheet: Record<string, unknown> };
|
|
||||||
|
|
||||||
return data.factSheet;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Für REST-Endpunkte (z.B. Bookmarks API) nutze `fetch` mit `getToken()`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const token = await getToken();
|
|
||||||
const res = await fetch(`${BEAM_BASE_URL}/services/pathfinder/v1/endpoint/${id}`, {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Tool registrieren in `src/index.ts`
|
|
||||||
|
|
||||||
Importiere die neue Funktion und registriere das Tool:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { myNewFunction } from "./api.js";
|
|
||||||
|
|
||||||
server.registerTool(
|
|
||||||
"beam_my_new_tool", // beam_ Prefix + snake_case
|
|
||||||
{
|
|
||||||
title: "Kurzer deutscher Titel",
|
|
||||||
description: `Ausführliche Beschreibung was das Tool tut.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- param1 (string, required): Beschreibung
|
|
||||||
- param2 (string, optional): Beschreibung
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Was zurückgegeben wird.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- param1="Beispielwert" → Was passiert`,
|
|
||||||
inputSchema: {
|
|
||||||
param1: z.string()
|
|
||||||
.min(1, "Darf nicht leer sein")
|
|
||||||
.describe("Beschreibung des Parameters"),
|
|
||||||
param2: z.string()
|
|
||||||
.optional()
|
|
||||||
.describe("Optionaler Parameter"),
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
readOnlyHint: true, // true wenn nur lesend
|
|
||||||
destructiveHint: false, // true wenn Daten gelöscht werden
|
|
||||||
idempotentHint: true, // true wenn wiederholbar ohne Seiteneffekte
|
|
||||||
openWorldHint: true, // true wenn externe Systeme angesprochen werden
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ param1, param2 }) => {
|
|
||||||
try {
|
|
||||||
const result = await myNewFunction(param1);
|
|
||||||
// Ergebnis formatieren...
|
|
||||||
const text = `## Ergebnis\n${JSON.stringify(result, null, 2)}`;
|
|
||||||
return { content: [{ type: "text", text: truncateIfNeeded(text) }] };
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Label-Mappings erweitern (falls nötig)
|
|
||||||
|
|
||||||
Wenn neue BEAM-Werte gemappt werden müssen, die bestehenden Maps in `index.ts` erweitern:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const LC: Record<string, string> = {
|
|
||||||
// ... bestehende Einträge
|
|
||||||
newValue: "Neuer Wert",
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Build & Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Muss fehlerfrei durchlaufen. Danach MCP-Server neu starten.
|
|
||||||
|
|
||||||
## 5. README aktualisieren
|
|
||||||
|
|
||||||
Neues Tool in `README.md` unter "Verfügbare Tools" dokumentieren:
|
|
||||||
- Tool-Name mit `beam_` Prefix
|
|
||||||
- Parameter-Tabelle
|
|
||||||
- Annotations
|
|
||||||
- Beispiele
|
|
||||||
- Beispiel-Ausgabe
|
|
||||||
|
|
||||||
## Checkliste
|
|
||||||
|
|
||||||
- [ ] API-Funktion in `api.ts` (nicht in `index.ts`)
|
|
||||||
- [ ] `server.registerTool()` mit title, description, inputSchema, annotations
|
|
||||||
- [ ] Tool-Name: `beam_` + snake_case
|
|
||||||
- [ ] Description mit Args, Returns, Examples
|
|
||||||
- [ ] Zod-Schema mit `.describe()` und Validierung
|
|
||||||
- [ ] Alle 4 Annotations gesetzt
|
|
||||||
- [ ] try/catch mit `handleError()`
|
|
||||||
- [ ] `truncateIfNeeded()` für große Responses
|
|
||||||
- [ ] `npm run build` erfolgreich
|
|
||||||
- [ ] README.md aktualisiert
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
---
|
|
||||||
inclusion: always
|
|
||||||
---
|
|
||||||
|
|
||||||
# BEAM Domänenwissen
|
|
||||||
|
|
||||||
## Was ist BEAM?
|
|
||||||
|
|
||||||
BEAM (Business Enterprise Architecture Management) ist die Architektur-Plattform der Deutschen Bahn, basierend auf LeanIX. Sie dokumentiert die IT-Landschaft mit Anwendungen, Domänen, IT-Komponenten, Schnittstellen und Diagrammen.
|
|
||||||
|
|
||||||
URL: https://db.leanix.net (intern: http://db.de/beam)
|
|
||||||
|
|
||||||
## Zentrale Konzepte
|
|
||||||
|
|
||||||
### Fact Sheets
|
|
||||||
|
|
||||||
Fact Sheets sind die Kernobjekte in BEAM. Jedes hat eine UUID und optional eine externe Beam-ID.
|
|
||||||
|
|
||||||
| Typ | Beam-ID Format | Beispiel | Beschreibung |
|
|
||||||
|-----|---------------|----------|--------------|
|
|
||||||
| Application | A-XXXXXX | A-106614 | Anwendung/Software-System |
|
|
||||||
| BusinessCapability | P.X.XX.XX.XX | P.F.05.05.04 | Fachliche Domäne/Capability |
|
|
||||||
| ITComponent | — | — | Technische Infrastruktur-Komponente |
|
|
||||||
| Interface | — | — | Schnittstelle zwischen Anwendungen |
|
|
||||||
| DataObject | — | — | Datenobjekt |
|
|
||||||
|
|
||||||
### Lebenszyklus
|
|
||||||
|
|
||||||
Jedes Fact Sheet hat einen Lebenszyklus-Status:
|
|
||||||
- `plan` → Plan
|
|
||||||
- `phaseIn` → Einführungsphase
|
|
||||||
- `active` → Aktiv
|
|
||||||
- `phaseOut` → Ausgliederungsphase
|
|
||||||
- `endOfLife` → Lebensende
|
|
||||||
|
|
||||||
### Supporttyp (Domänen-Zuordnung)
|
|
||||||
|
|
||||||
Anwendungen werden Domänen mit einem Supporttyp zugeordnet:
|
|
||||||
- `leading` → Führend (Hauptanwendung der Domäne)
|
|
||||||
- `supports` → Unterstützt (Hilfsanwendung)
|
|
||||||
|
|
||||||
### Diagramme
|
|
||||||
|
|
||||||
Diagramme in BEAM sind als "Bookmarks" gespeichert und enthalten mxGraph-XML (draw.io-kompatibel). Sie werden über die Documents-Relation eines Fact Sheets verknüpft. Typische Diagrammtypen:
|
|
||||||
- System Context (C4-Modell)
|
|
||||||
- Verteilungssicht (PROD/TST/ABN)
|
|
||||||
- Schnittstellen
|
|
||||||
|
|
||||||
## BEAM GraphQL API
|
|
||||||
|
|
||||||
Endpunkt: `https://db.leanix.net/services/pathfinder/v1/graphql`
|
|
||||||
|
|
||||||
Wichtige Queries:
|
|
||||||
- `allFactSheets(filter, first)` — Suche mit Volltextfilter
|
|
||||||
- `factSheet(id)` — Einzelnes Fact Sheet mit Relationen
|
|
||||||
- Relationen: `relApplicationToBusinessCapability`, `relBusinessCapabilityToApplication`, `relApplicationToITComponent`, `relToSuccessor`
|
|
||||||
|
|
||||||
## BEAM Bookmarks API (Diagramme)
|
|
||||||
|
|
||||||
Endpunkt: `https://db.leanix.net/services/pathfinder/v1/bookmarks/{id}`
|
|
||||||
|
|
||||||
Gibt ein JSON-Objekt zurück mit:
|
|
||||||
- `data.name` — Diagrammname
|
|
||||||
- `data.groupKey` — Typ (z.B. "freedraw")
|
|
||||||
- `data.state.graphXml` — mxGraph-XML (draw.io-kompatibel)
|
|
||||||
- `data.description` — Beschreibung
|
|
||||||
- `data.referencedFactSheetIds` — Verknüpfte Fact Sheets
|
|
||||||
|
|
||||||
## Terminologie-Mapping
|
|
||||||
|
|
||||||
Der MCP-Server mappt LeanIX-interne Begriffe auf BEAM-Terminologie:
|
|
||||||
- Application → Anwendung
|
|
||||||
- BusinessCapability → Domäne
|
|
||||||
- ITComponent → IT-Komponente
|
|
||||||
- Interface → Integration (Schnittstelle)
|
|
||||||
- lxState APPROVED → Bestätigt
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
---
|
|
||||||
inclusion: always
|
|
||||||
---
|
|
||||||
|
|
||||||
# Coding Standards — beam-mcp-server
|
|
||||||
|
|
||||||
## Projektstruktur
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── index.ts # MCP Server, Tool-Registrierung, Label-Mappings, Error-Handling
|
|
||||||
├── api.ts # BEAM GraphQL/REST API Client, Diagramm-Funktionen
|
|
||||||
├── auth.ts # Token-Management (Keychain + Playwright Browser-Login)
|
|
||||||
└── keychain.ts # OS-Keychain Abstraktionsschicht (macOS/Windows)
|
|
||||||
```
|
|
||||||
|
|
||||||
## TypeScript-Konventionen
|
|
||||||
|
|
||||||
- ESM (`"type": "module"` in package.json), alle Imports mit `.js`-Suffix
|
|
||||||
- Strict mode aktiviert, kein `any` — nutze `Record<string, unknown>` oder konkrete Interfaces
|
|
||||||
- Target: ES2022, Module: Node16
|
|
||||||
- Build: `npm run build` (tsc), Output in `dist/`
|
|
||||||
|
|
||||||
## MCP Tool-Konventionen
|
|
||||||
|
|
||||||
- Alle Tools nutzen `server.registerTool()` (NICHT `server.tool()`)
|
|
||||||
- Tool-Namen: `beam_` Prefix + snake_case (z.B. `beam_search_factsheets`)
|
|
||||||
- Jedes Tool braucht:
|
|
||||||
- `title`: Kurzer deutscher Titel
|
|
||||||
- `description`: Ausführliche Beschreibung mit Args, Returns, Examples
|
|
||||||
- `inputSchema`: Zod-Schemas mit `.describe()` und Validierung (`.min()`, `.max()`)
|
|
||||||
- `annotations`: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`
|
|
||||||
- Jeder Tool-Handler ist in try/catch gewrappt und nutzt `handleError()` bei Fehlern
|
|
||||||
- Große Responses werden mit `truncateIfNeeded()` auf CHARACTER_LIMIT (25k) gekürzt
|
|
||||||
|
|
||||||
## Error-Handling Pattern
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
async (params) => {
|
|
||||||
try {
|
|
||||||
// ... Logik ...
|
|
||||||
return { content: [{ type: "text", text: result }] };
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`handleError()` gibt `{ isError: true, content: [...] }` zurück mit kontextbezogenen Meldungen:
|
|
||||||
- 401/403 → Hinweis auf `beam_get_token`
|
|
||||||
- 404 → ID/Suchbegriff prüfen
|
|
||||||
- 429 → Rate-Limit, warten
|
|
||||||
|
|
||||||
## API-Schicht (api.ts)
|
|
||||||
|
|
||||||
- `gql()` — Shared GraphQL-Client mit Auth-Header
|
|
||||||
- Alle API-Funktionen sind `async` und werfen Errors mit HTTP-Status
|
|
||||||
- UUID-Erkennung: `/^[0-9a-f-]{36}$/i` — alles andere wird als Beam-ID/Name per Suche aufgelöst
|
|
||||||
- Neue API-Funktionen gehören in `api.ts`, nicht in `index.ts`
|
|
||||||
|
|
||||||
## Label-Mappings (index.ts)
|
|
||||||
|
|
||||||
BEAM-interne Werte werden auf deutsche Labels gemappt:
|
|
||||||
- `LC` — Lebenszyklus (active→Aktiv, phaseOut→Ausgliederungsphase, ...)
|
|
||||||
- `ST` — Supporttyp (leading→Führend, supports→Unterstützt)
|
|
||||||
- `FS_TYPE` — Fact-Sheet-Typen (Application→Anwendung, ...)
|
|
||||||
- `STATE` — Status (APPROVED→Bestätigt, ...)
|
|
||||||
|
|
||||||
## Auth (auth.ts) & Keychain (keychain.ts)
|
|
||||||
|
|
||||||
- Token-Priorität: In-Memory Cache → OS-Keychain → Playwright Browser-Login
|
|
||||||
- Token wird im OS-Keychain gespeichert (macOS: Keychain Access, Windows: DPAPI)
|
|
||||||
- Keine .env-Datei für Secrets — nur für Konfigurations-URLs
|
|
||||||
- Gültigkeit: JWT exp-Claim minus 60s Puffer
|
|
||||||
- Bei Invalidierung (401): Token aus Memory + Keychain gelöscht
|
|
||||||
- Logging nur auf stderr (stdio-Transport!)
|
|
||||||
|
|
||||||
## Build & Test
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build # TypeScript kompilieren
|
|
||||||
npm start # Server starten (stdio)
|
|
||||||
npm run login # Manueller Browser-Login + Token im Keychain speichern
|
|
||||||
```
|
|
||||||
|
|
||||||
Nach jeder Änderung: `npm run build` muss fehlerfrei durchlaufen.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
^[everything]
|
|
||||||
* @FlorianHofmann
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Component: beam-mcp
|
|
||||||
|
|
||||||
## Description
|
|
||||||
MCP server for LeanIX/BEAM enterprise architecture (educational reference only)
|
|
||||||
|
|
||||||
## Metadata
|
|
||||||
- **Deployment Target:** local-mcp-stdio
|
|
||||||
- **Upstream URL:** https://git.tech.rz.db.de/beta/ai/mcp/beam-mcp
|
|
||||||
- **Status:** active
|
|
||||||
|
|
||||||
## Interconnections
|
|
||||||
- (to be documented)
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- Part of bahn context in the Orchestrator monorepo
|
|
||||||
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
DB Inner Source Lizenz Version 1.0
|
|
||||||
|
|
||||||
_Fachautoren: Cornelius Schumacher, Schlomo Schapiro (DB Systel GmbH)_
|
|
||||||
|
|
||||||
Diese Inner-Source-Lizenz für die Deutsche Bahn („DBISL“) gilt für Werke
|
|
||||||
(im Sinne der nachfolgenden Begriffsbestimmung), die unter
|
|
||||||
DBISL-Bedingungen zur Verfügung gestellt werden. Das Werk darf nur in
|
|
||||||
der durch diese Lizenz gestatteten Form genutzt werden (insoweit eine
|
|
||||||
solche Nutzung dem Urheber vorbehalten ist).
|
|
||||||
|
|
||||||
Das Werk wird unter den Bedingungen dieser Lizenz zur Verfügung
|
|
||||||
gestellt, wenn der Lizenzgeber (im Sinne der nachfolgenden
|
|
||||||
Begriffsbestimmung) den folgenden Hinweis unmittelbar hinter dem
|
|
||||||
Urheberrechtshinweis dieses Werks anbringt:
|
|
||||||
|
|
||||||
„Lizenziert unter der DBISL“ oder alternativ „Licensed under the DBISL“
|
|
||||||
|
|
||||||
oder in einer anderen Form zum Ausdruck bringt, dass er es unter der
|
|
||||||
DBISL lizenzieren möchte.
|
|
||||||
|
|
||||||
== 1. Begriffsbestimmungen
|
|
||||||
|
|
||||||
Für diese Lizenz gelten folgende Begriffsbestimmungen:
|
|
||||||
|
|
||||||
* „Lizenz“: diese Lizenz.
|
|
||||||
* „Originalwerk“: das Werk oder die Software, die vom Lizenzgeber unter
|
|
||||||
dieser Lizenz verbreitet oder zugänglich gemacht wird, und zwar als
|
|
||||||
Quellcode und gegebenenfalls auch als ausführbarer Code.
|
|
||||||
* „Bearbeitungen“: die Werke oder Software, die der Lizenznehmer auf der
|
|
||||||
Grundlage des Originalwerks oder seiner Bearbeitungen schaffen kann. In
|
|
||||||
dieser Lizenz wird nicht festgelegt, wie umfangreich die Änderung oder
|
|
||||||
wie stark die Abhängigkeit vom Originalwerk für eine Einstufung als
|
|
||||||
Bearbeitung sein muss; dies bestimmt sich nach dem anwendbaren
|
|
||||||
Urheberrecht
|
|
||||||
* „Werk“: das Originalwerk oder seine Bearbeitungen.
|
|
||||||
* „Quellcode“: diejenige Form des Werkes, die zur Auffassung durch den
|
|
||||||
Menschen bestimmt ist und die am besten geeignet ist, um vom Menschen
|
|
||||||
verstanden und verändert zu werden.
|
|
||||||
* „Ausführbarer Code“: die — üblicherweise — kompilierte Form des Werks,
|
|
||||||
die von einem Computer als Programm ausgeführt werden soll.
|
|
||||||
* „Lizenzgeber“: die juristische Person innerhalb des DB Konzerns, die
|
|
||||||
das Werk unter der Lizenz verbreitet oder zugänglich macht.
|
|
||||||
* „Urheberrechtsinhaber/Autor“: jeder, der bestimmte von ihm selbst
|
|
||||||
entwickelte oder von Dritten vorgegebene Aufgabenstellungen in ein
|
|
||||||
Originalwerk umsetzt oder am Originalwerk eine Bearbeitung vornimmt.
|
|
||||||
* „Bearbeiter“: jeder, der das Werk unter der Lizenz verändert oder auf
|
|
||||||
andere Weise zur Schaffung einer Bearbeitung beiträgt. Jeder Autor ist
|
|
||||||
auch Bearbeiter.
|
|
||||||
* „Lizenznehmer“ („Sie“, „Ihnen“): jede juristische Person innerhalb des
|
|
||||||
DB Konzerns, die das Werk unter den Lizenzbedingungen nutzt.
|
|
||||||
* „Verbreitung“ oder „Zugänglichmachung“: alle Formen von Verkauf,
|
|
||||||
Überlassung, Verleih, Vermietung, Verbreitung, Weitergabe, Übermittlung
|
|
||||||
oder anderweitiger Online- oder Offline-Bereitstellung von
|
|
||||||
Vervielfältigungen des Werks oder Zugänglichmachung seiner wesentlichen
|
|
||||||
Funktionen für dritte natürliche oder juristische Personen.
|
|
||||||
* „Beitrag“: jedes urheberrechtliche Werk, einschließlich des
|
|
||||||
Originalwerks sowie jeglicher Änderungen, die der Bearbeiter vornimmt,
|
|
||||||
und die dem Lizenzgeber bewusst zur Aufnahme in das Werk eingereicht
|
|
||||||
werden.
|
|
||||||
* „DB“ oder „DB Konzern“: die Deutsche Bahn AG und alle mit ihr nach §
|
|
||||||
15 AktG verbundenen Unternehmen.
|
|
||||||
|
|
||||||
== 2. Umfang der Lizenzrechte
|
|
||||||
|
|
||||||
Der Lizenzgeber erteilt Ihnen hiermit eine weltweite, unentgeltliche,
|
|
||||||
nicht ausschließliche, unterlizenzierbare Lizenz, die Sie für
|
|
||||||
Geschäftszwecke des DB Konzerns berechtigt:
|
|
||||||
|
|
||||||
* das Werk uneingeschränkt zu nutzen,
|
|
||||||
* das Werk zu vervielfältigen,
|
|
||||||
* das Werk zu verändern und Bearbeitungen auf der Grundlage des Werks zu
|
|
||||||
schaffen,
|
|
||||||
* das Werk oder Vervielfältigungen davon innerhalb der DB zu verbreiten,
|
|
||||||
|
|
||||||
Für die Wahrnehmung dieser Rechte können beliebige, derzeit bekannte
|
|
||||||
oder künftige Medien, Träger und Formate verwendet werden, soweit das
|
|
||||||
geltende Recht dem nicht entgegensteht.
|
|
||||||
|
|
||||||
Der Lizenzgeber erteilt dem Lizenznehmer ein nicht ausschließliches,
|
|
||||||
unentgeltliches Nutzungsrecht an seinen Patenten, sofern dies zur
|
|
||||||
Ausübung der durch die Lizenz erteilten Nutzungsrechte am Werk notwendig
|
|
||||||
ist.
|
|
||||||
|
|
||||||
== 3. Zugänglichmachung des Quellcodes
|
|
||||||
|
|
||||||
Der Lizenzgeber kann das Werk entweder als Quellcode oder als
|
|
||||||
ausführbaren Code zur Verfügung stellen. Stellt er es als ausführbaren
|
|
||||||
Code zur Verfügung, so stellt er darüber hinaus eine maschinenlesbare
|
|
||||||
Kopie des Quellcodes für jedes von ihm verbreitete
|
|
||||||
Vervielfältigungsstück des Werks zur Verfügung, oder er verweist in
|
|
||||||
einem Vermerk im Anschluss an den dem Werk beigefügten
|
|
||||||
Urheberrechtshinweis auf einen Speicherort, an dem problemlos und
|
|
||||||
unentgeltlich auf den Quellcode zugegriffen werden kann, solange der
|
|
||||||
Lizenzgeber das Werk verbreitet oder zugänglich macht.
|
|
||||||
|
|
||||||
== 4. Einschränkungen des Urheberrechts
|
|
||||||
|
|
||||||
Es ist nicht Zweck dieser Lizenz, Ausnahmen oder Schranken der
|
|
||||||
ausschließlichen Rechte des Urhebers am Werk, die dem Lizenznehmer
|
|
||||||
zugutekommen, einzuschränken. Auch die Erschöpfung dieser Rechte bleibt
|
|
||||||
von dieser Lizenz unberührt.
|
|
||||||
|
|
||||||
== 5. Pflichten des Lizenznehmers
|
|
||||||
|
|
||||||
Die Einräumung der oben genannten Rechte ist an mehrere Beschränkungen
|
|
||||||
und Pflichten für den Lizenznehmer gebunden:
|
|
||||||
|
|
||||||
* Inner Source: Der Lizenznehmer darf das Werk ausschließlich für
|
|
||||||
Geschäftszwecke des DB Konzerns nutzen.
|
|
||||||
* Urheberrechtshinweis, Lizenztext, Nennung des Bearbeiters: Der
|
|
||||||
Lizenznehmer muss alle Urheberrechts-, Patent- oder Markenrechtshinweise
|
|
||||||
und alle Hinweise auf die Lizenz und den Haftungsausschluss unverändert
|
|
||||||
lassen. Jedem von ihm verbreiteten oder zugänglich gemachten
|
|
||||||
Vervielfältigungsstück des Werks muss der Lizenznehmer diese Hinweise
|
|
||||||
sowie diese Lizenz beifügen. Der Lizenznehmer muss auf jedem
|
|
||||||
abgeleiteten Werk deutlich darauf hinweisen, dass das Werk geändert
|
|
||||||
wurde, und das Datum der Bearbeitung angeben.
|
|
||||||
* „Copyleft“-Klausel: Der Lizenznehmer darf Vervielfältigungen des
|
|
||||||
Originalwerks oder Bearbeitungen nur unter den Bedingungen dieser DBISL
|
|
||||||
oder einer neueren Version dieser Lizenz innerhalb der DB verbreiten
|
|
||||||
oder zugänglich machen. Der Lizenznehmer (der zum Lizenzgeber wird) darf
|
|
||||||
für das Werk oder die Bearbeitung keine zusätzlichen Bedingungen
|
|
||||||
anbieten oder vorschreiben, die die Bedingungen dieser Lizenz verändern
|
|
||||||
oder einschränken.
|
|
||||||
* Bereitstellung des Quellcodes: Wenn der Lizenznehmer
|
|
||||||
Vervielfältigungsstücke des Werks verbreitet oder zugänglich macht, muss
|
|
||||||
er eine maschinenlesbare Fassung des Quellcodes mitliefern oder einen
|
|
||||||
Speicherort angeben, über den problemlos und unentgeltlich so lange auf
|
|
||||||
diesen Quellcode zugegriffen werden kann, wie der Lizenznehmer das Werk
|
|
||||||
verbreitet oder zugänglich macht.
|
|
||||||
* Rechtsschutz: Diese Lizenz erlaubt nicht die Benutzung von
|
|
||||||
Kennzeichen, Marken oder geschützten Namensrechten des Lizenzgebers,
|
|
||||||
soweit dies nicht für die angemessene und übliche Beschreibung der
|
|
||||||
Herkunft des Werks und der inhaltlichen Wiedergabe des
|
|
||||||
Urheberrechtshinweises erforderlich ist.
|
|
||||||
|
|
||||||
== 6. Urheber und Bearbeiter
|
|
||||||
|
|
||||||
Der ursprüngliche Lizenzgeber gewährleistet, dass er das Urheberrecht am
|
|
||||||
Originalwerk innehat oder dieses an ihn lizenziert wurde und dass er
|
|
||||||
befugt ist, diese Lizenz zu erteilen.
|
|
||||||
|
|
||||||
Jeder Bearbeiter gewährleistet, dass er das Urheberrecht an den von ihm
|
|
||||||
vorgenommenen Änderungen des Werks besitzt und befugt ist, einen Beitrag
|
|
||||||
unter dieser Lizenz zu erstellen und beizutragen.
|
|
||||||
|
|
||||||
Für jeden Fall, in dem der Lizenznehmer die Lizenz annimmt, erteilt der
|
|
||||||
ursprüngliche Lizenzgeber und alle folgenden Bearbeiter eine Befugnis
|
|
||||||
zur Nutzung der Beiträge zum Werk unter den Bedingungen dieser Lizenz.
|
|
||||||
|
|
||||||
== 7. Gewährleistungsausschluss
|
|
||||||
|
|
||||||
Falls die konzerninternen Leistungsbedingungen keine Anwendung finden,
|
|
||||||
gelten die folgenden Regelungen.
|
|
||||||
|
|
||||||
Die Arbeit an diesem Werk wird laufend fortgeführt; es wird durch
|
|
||||||
unzählige Bearbeiter ständig verbessert. Das Werk ist nicht vollendet
|
|
||||||
und kann daher Fehler („Bugs“) enthalten, die dieser Art der Entwicklung
|
|
||||||
inhärent sind.
|
|
||||||
|
|
||||||
Aus den genannten Gründen wird das Werk unter dieser Lizenz „so, wie es
|
|
||||||
ist“ ohne jegliche Gewährleistung zur Verfügung gestellt. Dies gilt
|
|
||||||
unter anderem — aber nicht ausschließlich — für Marktreife,
|
|
||||||
Verwendbarkeit für einen bestimmten Zweck, Mängelfreiheit, Richtigkeit
|
|
||||||
sowie Nichtverletzung von anderen Immaterialgüterrechten als dem
|
|
||||||
Urheberrecht (vgl. dazu Artikel 6 dieser Lizenz).
|
|
||||||
|
|
||||||
Dieser Gewährleistungsausschluss ist wesentlicher Bestandteil der Lizenz
|
|
||||||
und Bedingung für die Einräumung von Rechten an dem Werk.
|
|
||||||
|
|
||||||
== 8. Haftungsausschluss/Haftungsbeschränkung
|
|
||||||
|
|
||||||
Falls die konzerninternen Leistungsbedingungen keine Anwendung finden,
|
|
||||||
gelten die folgenden Regelungen.
|
|
||||||
|
|
||||||
Außer in Fällen von Vorsatz oder der Verursachung von Personenschäden
|
|
||||||
haftet der Lizenzgeber nicht für direkte oder indirekte, materielle oder
|
|
||||||
immaterielle Schäden irgendwelcher Art, die aus der Lizenz oder der
|
|
||||||
Benutzung des Werks folgen; dies gilt unter anderem, aber nicht
|
|
||||||
ausschließlich, für Firmenwertverluste, Produktionsausfall,
|
|
||||||
Computerausfall oder Computerfehler, Datenverlust oder wirtschaftliche
|
|
||||||
Schäden, und zwar auch dann, wenn der Lizenzgeber auf die Möglichkeit
|
|
||||||
solcher Schäden hingewiesen wurde. Unabhängig davon haftet der
|
|
||||||
Lizenzgeber im Rahmen der gesetzlichen Produkthaftung, soweit die
|
|
||||||
entsprechenden Regelungen auf das Werk anwendbar sind.
|
|
||||||
|
|
||||||
== 9. Zusatzvereinbarungen
|
|
||||||
|
|
||||||
Wenn der Lizenznehmer das Werk verbreitet, kann er Zusatzvereinbarungen
|
|
||||||
schließen, in denen Verpflichtungen oder Dienstleistungen festgelegt
|
|
||||||
werden, die mit dieser Lizenz vereinbar sind.
|
|
||||||
|
|
||||||
Der Lizenznehmer darf Verpflichtungen nur in seinem eigenen Namen
|
|
||||||
eingehen, nicht jedoch im Namen des ursprünglichen Lizenzgebers oder
|
|
||||||
eines anderen Bearbeiters, und nur, wenn er sich gegenüber allen
|
|
||||||
Bearbeitern verpflichtet, sie zu entschädigen, zu verteidigen und von
|
|
||||||
der Haftung freizustellen, falls aufgrund der von ihm eingegangenen
|
|
||||||
Gewährleistungsverpflichtung oder Haftungsübernahme Forderungen gegen
|
|
||||||
sie geltend gemacht werden oder eine Haftungsverpflichtung entsteht.
|
|
||||||
|
|
||||||
== 10. Annahme der Lizenz
|
|
||||||
|
|
||||||
Der Lizenznehmer stimmt den Bestimmungen dieser Lizenz zu, indem er das
|
|
||||||
Symbol „Lizenz annehmen“ unter dem Fenster mit dem Lizenztext anklickt
|
|
||||||
oder indem er seine Zustimmung auf vergleichbare Weise gibt. Das
|
|
||||||
Anklicken des Symbols gilt als Anzeichen der eindeutigen und
|
|
||||||
unwiderruflichen Annahme der Lizenz und der darin enthaltenen Klauseln
|
|
||||||
und Bedingungen.
|
|
||||||
|
|
||||||
In gleicher Weise gilt als Zeichen der eindeutigen und unwiderruflichen
|
|
||||||
Zustimmung die Ausübung eines Rechtes, das in Artikel 2 dieser Lizenz
|
|
||||||
angeführt ist, wie das Erstellen einer Bearbeitung oder die Verbreitung
|
|
||||||
oder Zugänglichmachung des Werks oder dessen Vervielfältigungen.
|
|
||||||
|
|
||||||
== 11. Informationspflichten
|
|
||||||
|
|
||||||
Wenn der Lizenznehmer das Werk verbreitet oder zugänglich macht
|
|
||||||
(beispielsweise, indem er es zum Herunterladen von einer Website
|
|
||||||
anbietet), muss der Lizenznehmer über den Vertriebskanal oder das
|
|
||||||
benutzte Verbreitungsmedium dem Adressatenkreis bzw. der Öffentlichkeit
|
|
||||||
Mindest-Informationen bereitstellen, üblicherweise bezüglich der
|
|
||||||
Lizenzgeber, der Lizenz und ihrer Zugänglichkeit, des Abschlusses des
|
|
||||||
Lizenzvertrags sowie darüber, wie die Lizenz durch den Lizenznehmer
|
|
||||||
gespeichert und vervielfältigt werden kann.
|
|
||||||
|
|
||||||
== 12. Beendigung der Lizenz
|
|
||||||
|
|
||||||
Die Lizenz und die damit eingeräumten Rechte erlöschen automatisch, wenn
|
|
||||||
der Lizenznehmer gegen die Lizenzbedingungen verstößt.
|
|
||||||
|
|
||||||
Ein solches Erlöschen der Lizenz führt nicht zum Erlöschen der Lizenzen
|
|
||||||
von Dritten, denen das Werk vom Lizenznehmer unter dieser Lizenz zur
|
|
||||||
Verfügung gestellt worden ist, solange diese Personen die
|
|
||||||
Lizenzbedingungen erfüllen.
|
|
||||||
|
|
||||||
== 13. Einreichung von Beiträgen
|
|
||||||
|
|
||||||
Sofern nichts ausdrücklich anderes angegeben, unterliegt jeder Beitrag,
|
|
||||||
den der Lizenzgeber bewusst zur Aufnahme in das Werk eingereicht hat,
|
|
||||||
den Bedingungen dieser Lizenz, ohne dass zusätzliche Bedingungen gelten.
|
|
||||||
Ungeachtet des Vorstehenden ersetzt oder ändert keine der hierin
|
|
||||||
enthaltenen Bestimmungen die Bedingungen einer separaten
|
|
||||||
Lizenzvereinbarung, die der Auftraggeber möglicherweise mit dem
|
|
||||||
Auftragnehmer für solche Beiträge abgeschlossen hat.
|
|
||||||
|
|
||||||
Für die Länder, in denen Urheberpersönlichkeitsrechte an einem Werk
|
|
||||||
entstehen können, verzichtet der Urheberrechtsinhaber/Autor im
|
|
||||||
gesetzlich zulässigen Umfang auf seine Urheberpersönlichkeitsrechte, um
|
|
||||||
die Lizenzierung der oben aufgeführten Verwertungsrechte wirksam
|
|
||||||
durchführen zu können.
|
|
||||||
|
|
||||||
== 14. Sonstiges
|
|
||||||
|
|
||||||
Unbeschadet des Artikels 9 stellt diese Lizenz die vollständige
|
|
||||||
Vereinbarung der Parteien über das Werk dar.
|
|
||||||
|
|
||||||
Es gilt deutsches Recht. Sind einzelne Bestimmungen der Lizenz nach
|
|
||||||
geltendem Recht nichtig oder unwirksam, so berührt dies nicht die
|
|
||||||
Wirksamkeit oder Durchsetzbarkeit der Lizenz an sich. Solche
|
|
||||||
Bestimmungen werden vielmehr dergestalt ausgelegt oder modifiziert, dass
|
|
||||||
sie wirksam und durchsetzbar sind.
|
|
||||||
|
|
||||||
== 15. Gesellschaftsrechtliche Veränderungen
|
|
||||||
|
|
||||||
Bei gesellschaftsrechtlichen Veränderungen, z.B. dem Verkauf oder der
|
|
||||||
Abspaltung einer DB Gesellschaft, gilt folgende Regelungen in Anlehnung
|
|
||||||
an §12 Beendigungsunterstützung der konzerninternen
|
|
||||||
Leistungsbedingungen:
|
|
||||||
|
|
||||||
Eine weitere Nutzung der lizenzierten Software durch ein nicht mehr dem
|
|
||||||
DB Konzern angehöriges Unternehmen unterliegt der Zustimmung durch die
|
|
||||||
Urheber bzw. das CIO Board.
|
|
||||||
|
|
||||||
== 16. Lizenzänderungen
|
|
||||||
|
|
||||||
Die Urheber eines Werks können gemeinsam eine Änderung der Lizenz
|
|
||||||
entscheiden, z.B. um das Werk als Open Source Software zu
|
|
||||||
veröffentlichen. Falls die Urheber nicht verfügbar sind oder sich nicht
|
|
||||||
einigen können, so kann das CIO Board stellvertretend für alle Urheber
|
|
||||||
innerhalb der DB die Änderung der Lizenz für ein Werk beschließen.
|
|
||||||
|
|
||||||
== 17. Streitbeilegung
|
|
||||||
|
|
||||||
Unbeschadet der Regelungen in den konzerninternen Leistungsbedingungen
|
|
||||||
zwischen den Parteien gilt zwischen den Parteien Folgendes:
|
|
||||||
|
|
||||||
Bei Streitigkeiten im Zusammenhang mit der Auslegung und Anwendung
|
|
||||||
dieser Lizenz, bei denen mehr als ein Konzernunternehmen beteiligt ist,
|
|
||||||
dient das CIO Board des Konzerns als Entscheidungsgremium, welches von
|
|
||||||
jeder Partei angerufen werden kann.
|
|
||||||
|
|
||||||
== 18. Lizenz der Lizenz
|
|
||||||
|
|
||||||
Dieser Lizenztext ist lizenziert unter einer
|
|
||||||
„https://creativecommons.org/licenses/by/4.0/[Creative Commons
|
|
||||||
Namensnennung 4.0 International Lizenz]“ (CC-BY 4.0).
|
|
||||||
|
|
||||||
Sie dürfen diesen Lizenztext für sich kopieren und anpassen, solange Sie
|
|
||||||
dabei die Deutsche Bahn Marke und „DB“ nur innerhalb der DB benutzen.
|
|
||||||
Falls Sie das Material für die Verwendung außerhalb der DB anpassen, so
|
|
||||||
müssen Sie alle Nennungen der Deutschen Bahn und DB ersetzen bzw.
|
|
||||||
entfernen. Geänderte Versionen des Lizenztextes müssen klar als
|
|
||||||
geänderte Versionen kenntlich gemacht werden.
|
|
||||||
|
|
||||||
Teile des Textes dieser Lizenz basieren auf der EU Public License (EUPL)
|
|
||||||
v1.2.
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
*DIESER MCP SERVER DIENT NUR FÜR "EDUCATIONAL PURPOSES"! EINE AKTIVE VERWENDUNG IST NICHT GESTATTET AUFGRUND DATENSCHUTZRECHTLICHER BEDENKEN!*
|
|
||||||
|
|
||||||
# BEAM MCP Server
|
|
||||||
|
|
||||||
MCP Server für die Deutsche Bahn Enterprise Architektur Plattform **BEAM** (http://db.de/beam).
|
|
||||||
Ermöglicht den direkten Zugriff auf BEAM-Daten über die LeanIX GraphQL API — ohne Browser-Navigation.
|
|
||||||
|
|
||||||
## Ansprechpartner
|
|
||||||
|
|
||||||
| Kontakt | E-Mail |
|
|
||||||
|---------|--------|
|
|
||||||
| Team DT-BETA | DBS.BS.INFRA.DS4E.DT-BETA@deutschebahn.com |
|
|
||||||
|
|
||||||
## Lizenz
|
|
||||||
|
|
||||||
DB Inner Source License (DBISL) - DB Systel GmbH
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Voraussetzungen
|
|
||||||
|
|
||||||
- Node.js >= 18
|
|
||||||
- Microsoft Edge (für automatischen Browser-Login)
|
|
||||||
- Zugang zu BEAM (DB-Netz oder VPN)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd beam-mcp
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Konfiguration
|
|
||||||
|
|
||||||
Optional: Kopiere `.env.example` nach `.env` um die Standard-URLs anzupassen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### Umgebungsvariablen
|
|
||||||
|
|
||||||
| Variable | Beschreibung | Standard |
|
|
||||||
|---|---|---|
|
|
||||||
| `BEAM_BASE_URL` | Basis-URL der BEAM-Instanz | `https://db.leanix.net` |
|
|
||||||
| `BEAM_LOGIN_URL` | Login-URL für automatischen Browser-Login | `http://db.de/beam` |
|
|
||||||
| `BEAM_WORKSPACE` | Workspace-Name | `beam` |
|
|
||||||
| `BEAM_HEADLESS` | Browser-Login im Headless-Modus (`true`/`false`). Bei Anmeldeproblemen auf `false` setzen, um den Browser sichtbar zu starten. | `true` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Authentifizierung
|
|
||||||
|
|
||||||
Das BEAM-Token wird sicher im **OS-Keychain** gespeichert:
|
|
||||||
|
|
||||||
- **macOS**: Keychain Access (via `security` CLI)
|
|
||||||
- **Windows**: DPAPI-verschlüsselte Datei in `%LOCALAPPDATA%/beam-mcp/`
|
|
||||||
|
|
||||||
### Automatischer Browser-Login (empfohlen)
|
|
||||||
|
|
||||||
Beim ersten Aufruf eines Tools ohne gültiges Token startet der Server automatisch einen Microsoft Edge Browser, navigiert zu BEAM und meldet sich über "BEAM: DB User" an. Das Token wird anschließend im OS-Keychain gespeichert und für alle weiteren Anfragen (auch nach Server-Neustart) wiederverwendet.
|
|
||||||
|
|
||||||
Falls der automatische Login fehlschlägt, kann der Browser mit `BEAM_HEADLESS=false` sichtbar gestartet werden, um das Problem zu diagnostizieren.
|
|
||||||
|
|
||||||
Der Login kann auch manuell ausgelöst werden:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run login
|
|
||||||
```
|
|
||||||
|
|
||||||
Oder über das Tool `beam_get_token` im MCP-Client.
|
|
||||||
|
|
||||||
### Token-Lebensdauer
|
|
||||||
|
|
||||||
BEAM-Tokens sind zeitlich begrenzt gültig. Der Server prüft die Gültigkeit automatisch und löst bei Bedarf einen neuen Login aus. Bei einem 401-Fehler wird das gespeicherte Token aus dem Keychain gelöscht und ein neuer Login getriggert.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Integration
|
|
||||||
|
|
||||||
### Kiro
|
|
||||||
|
|
||||||
In `.kiro/settings/mcp.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"beam-mcp": {
|
|
||||||
"command": "node",
|
|
||||||
"args": ["/pfad/zu/beam-mcp/dist/index.js"],
|
|
||||||
"env": {
|
|
||||||
"BEAM_BASE_URL": "https://db.leanix.net",
|
|
||||||
"BEAM_WORKSPACE": "beam",
|
|
||||||
"BEAM_HEADLESS": "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### OpenCode
|
|
||||||
|
|
||||||
In `.opencode/opencode.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"mcp": {
|
|
||||||
"beam-mcp": {
|
|
||||||
"enabled": true,
|
|
||||||
"type": "local",
|
|
||||||
"command": ["node", "/pfad/zu/beam-mcp/dist/index.js"],
|
|
||||||
"environment": {
|
|
||||||
"BEAM_BASE_URL": "https://db.leanix.net",
|
|
||||||
"BEAM_WORKSPACE": "beam",
|
|
||||||
"BEAM_HEADLESS": "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verfügbare Tools
|
|
||||||
|
|
||||||
Alle Tools verwenden das Prefix `beam_` um Namenskonflikte mit anderen MCP-Servern zu vermeiden.
|
|
||||||
|
|
||||||
### `beam_get_token`
|
|
||||||
|
|
||||||
Meldet sich bei BEAM an und speichert das Token im OS-Keychain.
|
|
||||||
Nur nötig, wenn kein gültiges Token vorhanden ist oder das Token abgelaufen ist.
|
|
||||||
|
|
||||||
**Parameter:** keine
|
|
||||||
|
|
||||||
**Annotations:** `readOnlyHint: false` | `idempotentHint: true`
|
|
||||||
|
|
||||||
**Beispiel:**
|
|
||||||
```
|
|
||||||
beam_get_token()
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ausgabe:**
|
|
||||||
```
|
|
||||||
✓ BEAM Login erfolgreich!
|
|
||||||
Benutzer: florian.hofmann@deutschebahn.com
|
|
||||||
Token gültig bis: 20.04.2026, 11:30:00
|
|
||||||
Token im Keychain gespeichert.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `beam_search_factsheets`
|
|
||||||
|
|
||||||
Sucht nach BEAM Fact Sheets anhand von Name, Beam-ID oder Freitext.
|
|
||||||
|
|
||||||
**Parameter:**
|
|
||||||
|
|
||||||
| Parameter | Typ | Pflicht | Beschreibung |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `query` | String | ✓ | Suchbegriff, Beam-ID (z.B. `A-106614`) oder Name |
|
|
||||||
| `type` | Enum | — | Einschränkung auf Fact-Sheet-Typ (siehe unten) |
|
|
||||||
|
|
||||||
**Mögliche Typen:**
|
|
||||||
- `Application` — Anwendung
|
|
||||||
- `BusinessCapability` — Domäne
|
|
||||||
- `ITComponent` — IT-Komponente
|
|
||||||
- `Interface` — Integration
|
|
||||||
- `DataObject` — Datenobjekt
|
|
||||||
|
|
||||||
**Annotations:** `readOnlyHint: true` | `idempotentHint: true`
|
|
||||||
|
|
||||||
**Beispiele:**
|
|
||||||
```
|
|
||||||
beam_search_factsheets(query: "A-106614")
|
|
||||||
beam_search_factsheets(query: "RiM", type: "Application")
|
|
||||||
beam_search_factsheets(query: "Personaleinsatz", type: "BusinessCapability")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `beam_get_factsheet`
|
|
||||||
|
|
||||||
Ruft vollständige Details eines BEAM Fact Sheets ab.
|
|
||||||
|
|
||||||
**Parameter:**
|
|
||||||
|
|
||||||
| Parameter | Typ | Pflicht | Beschreibung |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `id` | String | ✓ | Beam-ID (z.B. `A-106614`) oder UUID |
|
|
||||||
|
|
||||||
**Unterstützte Fact-Sheet-Typen:**
|
|
||||||
- Anwendungen (`Application`)
|
|
||||||
- Domänen (`BusinessCapability`)
|
|
||||||
- IT-Komponenten (`ITComponent`)
|
|
||||||
|
|
||||||
**Annotations:** `readOnlyHint: true` | `idempotentHint: true`
|
|
||||||
|
|
||||||
**Beispiele:**
|
|
||||||
```
|
|
||||||
beam_get_factsheet(id: "A-106614")
|
|
||||||
beam_get_factsheet(id: "33cb34f7-d6fe-481e-a650-8ca42884c4eb")
|
|
||||||
beam_get_factsheet(id: "P.F.05.05.04")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ausgabe (Anwendung):**
|
|
||||||
```
|
|
||||||
## A-106614 - Fahrendes Personal RiM
|
|
||||||
Typ: Anwendung
|
|
||||||
Status: Bestätigt
|
|
||||||
Alias: RiM; Rail in Motion; Rim; rim
|
|
||||||
Lebenszyklus: Ausgliederungsphase
|
|
||||||
|
|
||||||
Beschreibung:
|
|
||||||
Mit "Fahrendes Personal RiM" wird Fahrpersonalen und Triebfahrzeugführern...
|
|
||||||
|
|
||||||
Tags: Bordservice, Mobile Solutions Bordpersonale
|
|
||||||
|
|
||||||
### Domänen
|
|
||||||
• P.F.05.05.04 - Informationen bereitstellen | Supporttyp: Führend
|
|
||||||
|
|
||||||
### IT-Komponenten
|
|
||||||
• Enterprise Cloud Managed
|
|
||||||
|
|
||||||
### Nachfolger
|
|
||||||
• MERKUR (ID: 8c92a72a-...)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `beam_get_domain_applications`
|
|
||||||
|
|
||||||
Listet alle Anwendungen einer BEAM-Domäne auf, gruppiert nach Supporttyp.
|
|
||||||
|
|
||||||
**Parameter:**
|
|
||||||
|
|
||||||
| Parameter | Typ | Pflicht | Beschreibung |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `domain` | String | ✓ | Domänen-ID (z.B. `P.F.05.05.04`), Name oder UUID |
|
|
||||||
|
|
||||||
**Annotations:** `readOnlyHint: true` | `idempotentHint: true`
|
|
||||||
|
|
||||||
**Beispiele:**
|
|
||||||
```
|
|
||||||
beam_get_domain_applications(domain: "P.F.05.05.04")
|
|
||||||
beam_get_domain_applications(domain: "Informationen bereitstellen")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `beam_get_diagram`
|
|
||||||
|
|
||||||
Ruft Diagramme einer BEAM-Anwendung ab. Listet verfügbare Diagramme auf, zeigt eine lesbare Zusammenfassung (Systeme, Schnittstellen) und kann das Diagramm als `.drawio`-Datei speichern.
|
|
||||||
|
|
||||||
**Parameter:**
|
|
||||||
|
|
||||||
| Parameter | Typ | Pflicht | Beschreibung |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `application` | String | ✓ | Anwendungsname, Beam-ID (z.B. `A-109410`) oder UUID |
|
|
||||||
| `diagram_name` | String | — | Name oder Teil des Diagrammnamens zum Abrufen |
|
|
||||||
| `save_path` | String | — | Dateipfad zum Speichern als `.drawio`-Datei |
|
|
||||||
|
|
||||||
**Annotations:** `readOnlyHint: false` (kann Dateien schreiben) | `idempotentHint: true`
|
|
||||||
|
|
||||||
**Beispiele:**
|
|
||||||
|
|
||||||
Diagramme auflisten:
|
|
||||||
```
|
|
||||||
beam_get_diagram(application: "KEP_KEO_Revision")
|
|
||||||
```
|
|
||||||
|
|
||||||
Diagramm lesen:
|
|
||||||
```
|
|
||||||
beam_get_diagram(application: "KEP_KEO_Revision", diagram_name: "System Context")
|
|
||||||
```
|
|
||||||
|
|
||||||
Diagramm als draw.io speichern:
|
|
||||||
```
|
|
||||||
beam_get_diagram(application: "KEP_KEO_Revision", diagram_name: "System Context", save_path: "./kep-system-context.drawio")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ausgabe (Lesen):**
|
|
||||||
```
|
|
||||||
## A-109410 - KEP_KEO_Revision - System Context
|
|
||||||
Typ: freedraw
|
|
||||||
Beschreibung: Systemkontextdiagramm zu "Anwendung zur Integration..."
|
|
||||||
|
|
||||||
### Systeme/Akteure (10)
|
|
||||||
• A-109410 - KEP_KEO_Revision [Software System] — Kraftwerkseinsatzoptimierung
|
|
||||||
• A-107939 - ETRM_Marktdatendatenbank [Software System] — Energiemarktrelevante Daten
|
|
||||||
...
|
|
||||||
|
|
||||||
### Schnittstellen/Verbindungen (12)
|
|
||||||
• Auswahl gewählter Produktionsvolumen für D und D+1
|
|
||||||
• Marktpreise
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ausgabe (Speichern):**
|
|
||||||
```
|
|
||||||
✓ Diagramm gespeichert: /pfad/zu/kep-system-context.drawio
|
|
||||||
Die Datei kann direkt in draw.io geöffnet werden.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Hinweise:**
|
|
||||||
- Die `.drawio`-Datei enthält das originale mxGraph-XML aus BEAM und kann direkt in draw.io (Desktop oder Web) geöffnet werden.
|
|
||||||
- Die lesbare Zusammenfassung extrahiert automatisch C4-Modell-Elemente (Systeme, Personen) und Schnittstellen.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fehlerbehandlung
|
|
||||||
|
|
||||||
Alle Tools geben bei Fehlern strukturierte Fehlermeldungen mit `isError: true` zurück:
|
|
||||||
|
|
||||||
| Fehler | Meldung | Lösung |
|
|
||||||
|--------|---------|--------|
|
|
||||||
| 401/403 | Authentifizierung fehlgeschlagen | `beam_get_token` aufrufen |
|
|
||||||
| 404 | Nicht gefunden | ID oder Suchbegriff prüfen |
|
|
||||||
| 429 | Rate-Limit erreicht | Kurz warten, erneut versuchen |
|
|
||||||
|
|
||||||
Große Antworten werden automatisch auf 25.000 Zeichen gekürzt mit einem Hinweis auf spezifischere Filter.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architektur
|
|
||||||
|
|
||||||
```
|
|
||||||
beam-mcp/
|
|
||||||
├── src/
|
|
||||||
│ ├── index.ts # MCP Server, Tool-Registrierung (registerTool), Error-Handling
|
|
||||||
│ ├── api.ts # BEAM GraphQL API Client, Diagramm-Export
|
|
||||||
│ ├── auth.ts # Authentifizierung (Keychain + Playwright Login)
|
|
||||||
│ └── keychain.ts # OS-Keychain Abstraktionsschicht (macOS/Windows)
|
|
||||||
├── dist/ # Kompilierter JavaScript-Code (nach npm run build)
|
|
||||||
├── .env.example # Vorlage für optionale Konfiguration
|
|
||||||
├── package.json
|
|
||||||
└── tsconfig.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Datenfluss
|
|
||||||
|
|
||||||
```
|
|
||||||
Kiro / OpenCode / KI-Assistent
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
beam-mcp-server (MCP über stdio)
|
|
||||||
│
|
|
||||||
├─► auth.ts ──► keychain.ts ──► OS-Keychain (Token lesen/schreiben)
|
|
||||||
│ └─► Playwright Edge Login (falls kein gültiges Token)
|
|
||||||
│
|
|
||||||
└─► api.ts ──► BEAM GraphQL API (https://db.leanix.net/services/pathfinder/v1/graphql)
|
|
||||||
└─► BEAM Bookmarks API (Diagramme)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Hinweise
|
|
||||||
|
|
||||||
- Das Token läuft nach einigen Stunden ab. Der Server erkennt dies automatisch und löst einen neuen Login aus.
|
|
||||||
- Der Browser-Login benötigt Zugang zu `http://db.de/beam` (DB-Netz oder VPN).
|
|
||||||
- Alle internen Variablen- und Feldnamen folgen der LeanIX API-Konvention; die Anzeige ist auf BEAM-Terminologie gemappt.
|
|
||||||
- Der Server ist read-only — es werden keine Daten in BEAM verändert (außer `.drawio`-Dateien lokal speichern).
|
|
||||||
- Alle Tools nutzen `registerTool` mit Annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`).
|
|
||||||
- Tool-Namen verwenden das `beam_`-Prefix zur Vermeidung von Namenskonflikten.
|
|
||||||
- Keine Secrets in Dateien — Token wird ausschließlich im OS-Keychain gespeichert.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "beam-mcp",
|
|
||||||
"version": "1.1.0",
|
|
||||||
"description": "MCP Server für BEAM (Deutsche Bahn Enterprise Architektur)",
|
|
||||||
"type": "module",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"start": "node dist/index.js",
|
|
||||||
"dev": "tsc --watch",
|
|
||||||
"login": "npm run build && node dist/login.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
||||||
"playwright": "^1.59.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^22.0.0",
|
|
||||||
"typescript": "^5.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
version: v3
|
|
||||||
confidentiality: internal
|
|
||||||
contacts: florian.hofmann@deutschebahn.com
|
|
||||||
license: LicenseRef-DBPROPRIETARY
|
|
||||||
reference-ids: none
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
import { getToken, invalidateToken, BEAM_BASE_URL } from "./auth.js";
|
|
||||||
import { writeFileSync } from "fs";
|
|
||||||
import { resolve } from "path";
|
|
||||||
|
|
||||||
const GRAPHQL_URL = `${BEAM_BASE_URL}/services/pathfinder/v1/graphql`;
|
|
||||||
|
|
||||||
async function gql(query: string, variables?: Record<string, unknown>, retry = true): Promise<unknown> {
|
|
||||||
const token = await getToken();
|
|
||||||
const res = await fetch(GRAPHQL_URL, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ query, variables }),
|
|
||||||
});
|
|
||||||
if (res.status === 401 && retry) {
|
|
||||||
invalidateToken();
|
|
||||||
return gql(query, variables, false);
|
|
||||||
}
|
|
||||||
if (!res.ok) throw new Error(`GraphQL HTTP ${res.status}: ${await res.text()}`);
|
|
||||||
const json = (await res.json()) as { data?: unknown; errors?: unknown[] };
|
|
||||||
if (json.errors?.length) throw new Error(`GraphQL errors: ${JSON.stringify(json.errors)}`);
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Search factsheets ────────────────────────────────────────────────────────
|
|
||||||
export async function searchFactSheets(query: string, type?: string) {
|
|
||||||
const filter: Record<string, unknown> = { fullTextSearch: query };
|
|
||||||
if (type) filter.facetFilters = [{ facetKey: "FactSheetTypes", keys: [type] }];
|
|
||||||
|
|
||||||
const data = (await gql(
|
|
||||||
`query Search($filter: FilterInput) {
|
|
||||||
allFactSheets(filter: $filter, first: 20) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
id name type
|
|
||||||
... on Application {
|
|
||||||
lxState
|
|
||||||
externalId { externalId }
|
|
||||||
alias
|
|
||||||
lifecycle { asString }
|
|
||||||
}
|
|
||||||
... on BusinessCapability {
|
|
||||||
externalId { externalId }
|
|
||||||
lifecycle { asString }
|
|
||||||
}
|
|
||||||
... on ITComponent {
|
|
||||||
externalId { externalId }
|
|
||||||
lifecycle { asString }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
{ filter }
|
|
||||||
)) as { allFactSheets: { edges: { node: Record<string, unknown> }[] } };
|
|
||||||
|
|
||||||
return data.allFactSheets.edges.map((e) => e.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Get full factsheet details ───────────────────────────────────────────────
|
|
||||||
export async function getFactSheet(idOrBeamId: string) {
|
|
||||||
let uuid = idOrBeamId;
|
|
||||||
if (!idOrBeamId.match(/^[0-9a-f-]{36}$/i)) {
|
|
||||||
const results = await searchFactSheets(idOrBeamId);
|
|
||||||
if (!results.length) throw new Error(`No factsheet found for: ${idOrBeamId}`);
|
|
||||||
uuid = results[0].id as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await gql(
|
|
||||||
`query GetFS($id: ID!) {
|
|
||||||
factSheet(id: $id) {
|
|
||||||
id name type
|
|
||||||
... on Application {
|
|
||||||
lxState
|
|
||||||
externalId { externalId }
|
|
||||||
alias
|
|
||||||
description
|
|
||||||
lifecycle { asString }
|
|
||||||
tags { name }
|
|
||||||
relApplicationToBusinessCapability {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
supportType
|
|
||||||
functionalSuitability
|
|
||||||
factSheet {
|
|
||||||
id name
|
|
||||||
... on BusinessCapability { externalId { externalId } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
relApplicationToITComponent {
|
|
||||||
edges { node { factSheet { id name } } }
|
|
||||||
}
|
|
||||||
relToSuccessor {
|
|
||||||
edges { node { factSheet { id name } } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
... on BusinessCapability {
|
|
||||||
externalId { externalId }
|
|
||||||
description
|
|
||||||
lifecycle { asString }
|
|
||||||
relBusinessCapabilityToApplication {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
supportType
|
|
||||||
functionalSuitability
|
|
||||||
factSheet {
|
|
||||||
id name type
|
|
||||||
... on Application {
|
|
||||||
lxState
|
|
||||||
externalId { externalId }
|
|
||||||
alias
|
|
||||||
lifecycle { asString }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
... on ITComponent {
|
|
||||||
externalId { externalId }
|
|
||||||
description
|
|
||||||
lifecycle { asString }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
{ id: uuid }
|
|
||||||
)) as { factSheet: Record<string, unknown> };
|
|
||||||
|
|
||||||
return data.factSheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Get all applications of a domain ────────────────────────────────────────
|
|
||||||
export async function getDomainApplications(domainIdOrExternalId: string) {
|
|
||||||
let uuid = domainIdOrExternalId;
|
|
||||||
if (!domainIdOrExternalId.match(/^[0-9a-f-]{36}$/i)) {
|
|
||||||
const results = await searchFactSheets(domainIdOrExternalId, "BusinessCapability");
|
|
||||||
if (!results.length) throw new Error(`No domain found for: ${domainIdOrExternalId}`);
|
|
||||||
uuid = results[0].id as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await gql(
|
|
||||||
`query DomainApps($id: ID!) {
|
|
||||||
factSheet(id: $id) {
|
|
||||||
id name
|
|
||||||
... on BusinessCapability {
|
|
||||||
externalId { externalId }
|
|
||||||
relBusinessCapabilityToApplication {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
supportType
|
|
||||||
functionalSuitability
|
|
||||||
factSheet {
|
|
||||||
id name type
|
|
||||||
... on Application {
|
|
||||||
lxState
|
|
||||||
externalId { externalId }
|
|
||||||
alias
|
|
||||||
lifecycle { asString }
|
|
||||||
tags { name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
{ id: uuid }
|
|
||||||
)) as { factSheet: Record<string, unknown> };
|
|
||||||
|
|
||||||
return data.factSheet;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Get diagram (bookmark) ──────────────────────────────────────────────────
|
|
||||||
export async function getDiagram(id: string) {
|
|
||||||
const token = await getToken();
|
|
||||||
const res = await fetch(
|
|
||||||
`${BEAM_BASE_URL}/services/pathfinder/v1/bookmarks/${id}`,
|
|
||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
|
||||||
);
|
|
||||||
if (!res.ok) throw new Error(`Bookmark HTTP ${res.status}: ${await res.text()}`);
|
|
||||||
return (await res.json()) as { data: Record<string, unknown> };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Get diagrams linked to a fact sheet via documents ───────────────────────
|
|
||||||
export async function getFactSheetDiagrams(idOrName: string) {
|
|
||||||
let uuid = idOrName;
|
|
||||||
if (!idOrName.match(/^[0-9a-f-]{36}$/i)) {
|
|
||||||
const results = await searchFactSheets(idOrName);
|
|
||||||
if (!results.length) throw new Error(`No factsheet found for: ${idOrName}`);
|
|
||||||
uuid = results[0].id as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = (await gql(
|
|
||||||
`query Docs($id: ID!) {
|
|
||||||
factSheet(id: $id) { id name documents { edges { node { id name } } } }
|
|
||||||
}`,
|
|
||||||
{ id: uuid }
|
|
||||||
)) as { factSheet: { id: string; name: string; documents: { edges: { node: { id: string; name: string } }[] } } };
|
|
||||||
|
|
||||||
const diagramDocs = data.factSheet.documents.edges
|
|
||||||
.filter((e) => e.node.name.startsWith("Diagram "))
|
|
||||||
.map((e) => e.node.name.replace("Diagram ", ""));
|
|
||||||
|
|
||||||
const diagrams: { id: string; name: string; groupKey: string }[] = [];
|
|
||||||
for (const bmId of diagramDocs) {
|
|
||||||
try {
|
|
||||||
const bm = (await getDiagram(bmId)).data;
|
|
||||||
diagrams.push({ id: bmId, name: bm.name as string, groupKey: bm.groupKey as string });
|
|
||||||
} catch { /* skip inaccessible */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { factSheet: data.factSheet, diagrams };
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ── Extract graphXml from diagram bookmark data ─────────────────────────────
|
|
||||||
export function extractGraphXml(bookmarkData: Record<string, unknown>): string | null {
|
|
||||||
const state = bookmarkData.state as Record<string, unknown> | undefined;
|
|
||||||
if (!state?.graphXml) return null;
|
|
||||||
return state.graphXml as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Save diagram as .drawio file ────────────────────────────────────────────
|
|
||||||
export function saveDiagramAsDrawio(
|
|
||||||
graphXml: string,
|
|
||||||
diagramName: string,
|
|
||||||
outputPath: string
|
|
||||||
): string {
|
|
||||||
// draw.io expects the mxGraphModel wrapped in a <diagram> inside <mxfile>
|
|
||||||
const safeName = diagramName.replace(/[<>&"']/g, "");
|
|
||||||
const drawioXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<mxfile host="beam-mcp" modified="${new Date().toISOString()}" type="device">
|
|
||||||
<diagram name="${safeName}">
|
|
||||||
${graphXml}
|
|
||||||
</diagram>
|
|
||||||
</mxfile>`;
|
|
||||||
|
|
||||||
const fullPath = resolve(outputPath);
|
|
||||||
writeFileSync(fullPath, drawioXml, "utf8");
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Parse diagram into readable text summary ────────────────────────────────
|
|
||||||
export function parseDiagramToText(bookmarkData: Record<string, unknown>): string {
|
|
||||||
const lines: string[] = [];
|
|
||||||
const name = bookmarkData.name as string ?? "Unbekannt";
|
|
||||||
const desc = bookmarkData.description as string ?? "";
|
|
||||||
const groupKey = bookmarkData.groupKey as string ?? "";
|
|
||||||
const updatedAt = bookmarkData.updatedAt as string ?? "";
|
|
||||||
|
|
||||||
lines.push(`## ${name}`);
|
|
||||||
if (groupKey) lines.push(`Typ: ${groupKey}`);
|
|
||||||
if (desc) lines.push(`Beschreibung: ${desc}`);
|
|
||||||
if (updatedAt) lines.push(`Zuletzt aktualisiert: ${updatedAt}`);
|
|
||||||
|
|
||||||
// Parse graphXml to extract nodes and edges
|
|
||||||
const graphXml = extractGraphXml(bookmarkData);
|
|
||||||
if (!graphXml) {
|
|
||||||
lines.push("\nKein graphXml im Diagramm gefunden.");
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract nodes (factSheet objects with c4Name)
|
|
||||||
const nodeRegex = /c4Name="([^"]*)"[^>]*c4Type="([^"]*)"[^>]*c4Description="([^"]*)"[^>]*factSheetId="([^"]*)"/g;
|
|
||||||
const nodes: { name: string; type: string; desc: string; id: string }[] = [];
|
|
||||||
let match: RegExpExecArray | null;
|
|
||||||
while ((match = nodeRegex.exec(graphXml)) !== null) {
|
|
||||||
nodes.push({
|
|
||||||
name: match[1].replace(/&/g, "&").replace(/ /g, " ").replace(/<[^>]*>/g, ""),
|
|
||||||
type: match[2],
|
|
||||||
desc: match[3].replace(/&/g, "&").replace(/ /g, " ").replace(/<[^>]*>/g, ""),
|
|
||||||
id: match[4],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract edges (interface labels)
|
|
||||||
const edgeRegex = /label="([^"]*)"[^>]*factSheetType="Interface"[^>]*factSheetId="([^"]*)"/g;
|
|
||||||
const edges: { label: string; id: string }[] = [];
|
|
||||||
while ((match = edgeRegex.exec(graphXml)) !== null) {
|
|
||||||
edges.push({
|
|
||||||
label: match[1].replace(/&/g, "&").replace(/ /g, " "),
|
|
||||||
id: match[2],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodes.length) {
|
|
||||||
lines.push(`\n### Systeme/Akteure (${nodes.length})`);
|
|
||||||
nodes.forEach((n) => {
|
|
||||||
lines.push(` • ${n.name} [${n.type}]${n.desc ? " — " + n.desc : ""}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (edges.length) {
|
|
||||||
lines.push(`\n### Schnittstellen/Verbindungen (${edges.length})`);
|
|
||||||
edges.forEach((e) => {
|
|
||||||
lines.push(` • ${e.label}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
/**
|
|
||||||
* Auth-Modul: Stellt ein gültiges BEAM JWT-Token bereit.
|
|
||||||
*
|
|
||||||
* Priorität:
|
|
||||||
* 1. In-Memory Cache (schnellster Zugriff)
|
|
||||||
* 2. OS-Keychain (macOS Keychain / Windows DPAPI)
|
|
||||||
* 3. Playwright Browser-Login (automatisch wenn kein Token vorhanden)
|
|
||||||
*
|
|
||||||
* Token wird nach erfolgreichem Login im OS-Keychain persistiert.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { existsSync, mkdirSync } from "fs";
|
|
||||||
import { join, dirname } from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import { tmpdir } from "os";
|
|
||||||
import { getStoredToken, storeToken, deleteStoredToken } from "./keychain.js";
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
|
|
||||||
export const BEAM_BASE_URL = process.env.BEAM_BASE_URL ?? "https://db.leanix.net";
|
|
||||||
export const BEAM_LOGIN_URL = process.env.BEAM_LOGIN_URL ?? "http://db.de/beam";
|
|
||||||
const WORKSPACE = process.env.BEAM_WORKSPACE ?? "beam";
|
|
||||||
const TOKEN_KEY = `lxAccessToken:${WORKSPACE}`;
|
|
||||||
|
|
||||||
// Temporäres Profil-Verzeichnis (kein Konflikt mit laufendem Edge)
|
|
||||||
const TEMP_PROFILE_DIR = join(tmpdir(), "beam-mcp-edge-profile");
|
|
||||||
|
|
||||||
// In-Memory Cache
|
|
||||||
let cachedToken: string | null = null;
|
|
||||||
|
|
||||||
// ── Token-Gültigkeit prüfen ───────────────────────────────────────────────────
|
|
||||||
function parseExpiry(token: string): number {
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
||||||
return (payload.exp ?? 0) * 1000;
|
|
||||||
} catch {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValid(token: string): boolean {
|
|
||||||
return parseExpiry(token) > Date.now() + 60_000;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Playwright Login ──────────────────────────────────────────────────────────
|
|
||||||
export async function loginWithBrowser(): Promise<string> {
|
|
||||||
process.stderr.write("[BEAM] Kein gültiges Token gefunden. Starte Edge Login...\n");
|
|
||||||
|
|
||||||
const { chromium } = await import("playwright");
|
|
||||||
|
|
||||||
if (!existsSync(TEMP_PROFILE_DIR)) mkdirSync(TEMP_PROFILE_DIR, { recursive: true });
|
|
||||||
process.stderr.write(`[BEAM] Temporäres Profil: ${TEMP_PROFILE_DIR}\n`);
|
|
||||||
|
|
||||||
const context = await chromium.launchPersistentContext(TEMP_PROFILE_DIR, {
|
|
||||||
channel: "msedge",
|
|
||||||
headless: false,
|
|
||||||
ignoreDefaultArgs: [
|
|
||||||
"--disable-background-networking",
|
|
||||||
"--disable-extensions",
|
|
||||||
"--disable-sync",
|
|
||||||
"--no-sandbox",
|
|
||||||
],
|
|
||||||
args: ["--no-first-run", "--no-default-browser-check"],
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const page = await context.newPage();
|
|
||||||
|
|
||||||
await page.goto(BEAM_LOGIN_URL, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
||||||
|
|
||||||
// Klicke auf "BEAM: DB User" falls Login-Seite erscheint
|
|
||||||
const loginLink = page.getByRole("link", { name: /BEAM.*DB User/i })
|
|
||||||
.or(page.getByText(/BEAM.*DB User/i))
|
|
||||||
.first();
|
|
||||||
|
|
||||||
const isLoginPage = await loginLink.isVisible({ timeout: 5000 }).catch(() => false);
|
|
||||||
if (isLoginPage) {
|
|
||||||
await loginLink.click({ timeout: 10000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warte auf erfolgreichen Login (URL enthält leanix.net)
|
|
||||||
await page.waitForURL(/leanix\.net/, { timeout: 180000 });
|
|
||||||
|
|
||||||
// Warte bis Token im localStorage liegt
|
|
||||||
await page.waitForFunction(
|
|
||||||
(key) => !!localStorage.getItem(key),
|
|
||||||
TOKEN_KEY,
|
|
||||||
{ timeout: 30000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = await page.evaluate((key) => localStorage.getItem(key), TOKEN_KEY);
|
|
||||||
if (!token) throw new Error("Token nicht im localStorage gefunden nach Login.");
|
|
||||||
|
|
||||||
// Token im Keychain persistieren
|
|
||||||
storeToken(token);
|
|
||||||
process.stderr.write("[BEAM] Login erfolgreich. Token im Keychain gespeichert.\n");
|
|
||||||
return token;
|
|
||||||
} finally {
|
|
||||||
await context.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Haupt-Funktion: Token bereitstellen ──────────────────────────────────────
|
|
||||||
export async function getToken(): Promise<string> {
|
|
||||||
// 1. In-Memory Cache
|
|
||||||
if (cachedToken && isValid(cachedToken)) return cachedToken;
|
|
||||||
|
|
||||||
// 2. OS-Keychain
|
|
||||||
const keychainToken = getStoredToken();
|
|
||||||
if (keychainToken && isValid(keychainToken)) {
|
|
||||||
cachedToken = keychainToken;
|
|
||||||
process.stderr.write("[BEAM] Token aus Keychain geladen.\n");
|
|
||||||
return cachedToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Playwright Login
|
|
||||||
const token = await loginWithBrowser();
|
|
||||||
cachedToken = token;
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Token invalidieren (bei 401) ─────────────────────────────────────────────
|
|
||||||
export function invalidateToken(): void {
|
|
||||||
cachedToken = null;
|
|
||||||
deleteStoredToken();
|
|
||||||
process.stderr.write("[BEAM] Token invalidiert (Memory + Keychain).\n");
|
|
||||||
}
|
|
||||||
@@ -1,480 +0,0 @@
|
|||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
searchFactSheets,
|
|
||||||
getFactSheet,
|
|
||||||
getDomainApplications,
|
|
||||||
getDiagram,
|
|
||||||
getFactSheetDiagrams,
|
|
||||||
extractGraphXml,
|
|
||||||
saveDiagramAsDrawio,
|
|
||||||
parseDiagramToText,
|
|
||||||
} from "./api.js";
|
|
||||||
import { getToken, loginWithBrowser, invalidateToken } from "./auth.js";
|
|
||||||
|
|
||||||
const CHARACTER_LIMIT = 25_000;
|
|
||||||
|
|
||||||
const server = new McpServer({
|
|
||||||
name: "beam-mcp-server",
|
|
||||||
version: "1.1.0",
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Label-Mappings ────────────────────────────────────────────────────────────
|
|
||||||
const LC: Record<string, string> = {
|
|
||||||
active: "Aktiv",
|
|
||||||
plan: "Plan",
|
|
||||||
phaseIn: "Einführungsphase",
|
|
||||||
phaseOut: "Ausgliederungsphase",
|
|
||||||
endOfLife: "Lebensende",
|
|
||||||
};
|
|
||||||
const lcLabel = (s: string | undefined) => (s ? (LC[s] ?? s) : "—");
|
|
||||||
|
|
||||||
const ST: Record<string, string> = {
|
|
||||||
leading: "Führend",
|
|
||||||
supports: "Unterstützt",
|
|
||||||
};
|
|
||||||
const stLabel = (s: string | null | undefined) => (s ? (ST[s] ?? s) : "—");
|
|
||||||
|
|
||||||
const FS_TYPE: Record<string, string> = {
|
|
||||||
Application: "Anwendung",
|
|
||||||
BusinessCapability: "Domäne",
|
|
||||||
ITComponent: "IT-Komponente",
|
|
||||||
Interface: "Integration",
|
|
||||||
DataObject: "Datenobjekt",
|
|
||||||
TechnicalStack: "Technologie-Stack",
|
|
||||||
UserGroup: "Nutzergruppe",
|
|
||||||
Process: "Prozess",
|
|
||||||
Epic: "Initiative",
|
|
||||||
};
|
|
||||||
const typeLabel = (s: string) => FS_TYPE[s] ?? s;
|
|
||||||
|
|
||||||
const STATE: Record<string, string> = {
|
|
||||||
APPROVED: "Bestätigt",
|
|
||||||
DRAFT: "Entwurf",
|
|
||||||
ARCHIVED: "Archiviert",
|
|
||||||
};
|
|
||||||
const stateLabel = (s: string | undefined) => (s ? (STATE[s] ?? s) : "—");
|
|
||||||
|
|
||||||
// ── Shared error handler ──────────────────────────────────────────────────────
|
|
||||||
function handleError(error: unknown): { content: [{ type: "text"; text: string }]; isError: true } {
|
|
||||||
const msg = error instanceof Error ? error.message : String(error);
|
|
||||||
|
|
||||||
if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) {
|
|
||||||
return {
|
|
||||||
isError: true,
|
|
||||||
content: [{ type: "text", text: `Fehler: Authentifizierung fehlgeschlagen. Bitte 'beam_get_token' aufrufen, um ein neues Token zu erhalten.\nDetails: ${msg}` }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (msg.includes("HTTP 404") || msg.includes("No factsheet found") || msg.includes("No domain found")) {
|
|
||||||
return {
|
|
||||||
isError: true,
|
|
||||||
content: [{ type: "text", text: `Fehler: Nicht gefunden. Bitte prüfe die ID oder den Suchbegriff.\nDetails: ${msg}` }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (msg.includes("HTTP 429")) {
|
|
||||||
return {
|
|
||||||
isError: true,
|
|
||||||
content: [{ type: "text", text: `Fehler: Rate-Limit erreicht. Bitte kurz warten und erneut versuchen.` }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isError: true,
|
|
||||||
content: [{ type: "text", text: `Fehler: ${msg}` }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Truncation helper ─────────────────────────────────────────────────────────
|
|
||||||
function truncateIfNeeded(text: string): string {
|
|
||||||
if (text.length <= CHARACTER_LIMIT) return text;
|
|
||||||
return text.slice(0, CHARACTER_LIMIT) + "\n\n⚠ Ausgabe wurde auf 25.000 Zeichen gekürzt. Nutze spezifischere Filter.";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ── Tool: beam_get_token ──────────────────────────────────────────────────────
|
|
||||||
server.registerTool(
|
|
||||||
"beam_get_token",
|
|
||||||
{
|
|
||||||
title: "BEAM Token abrufen",
|
|
||||||
description: `Meldet sich automatisch bei BEAM an (headless Edge Browser) und hält das Token im Speicher (keine .env Persistenz).
|
|
||||||
Nur nötig wenn kein gültiges Token vorhanden ist oder ein 401/403-Fehler auftritt.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Bestätigung mit Benutzername und Token-Ablaufzeit.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- Nutze dieses Tool wenn andere BEAM-Tools mit Authentifizierungsfehlern fehlschlagen.
|
|
||||||
- Nutze es proaktiv vor der ersten BEAM-Abfrage.`,
|
|
||||||
inputSchema: {},
|
|
||||||
annotations: {
|
|
||||||
readOnlyHint: false,
|
|
||||||
destructiveHint: false,
|
|
||||||
idempotentHint: true,
|
|
||||||
openWorldHint: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
try {
|
|
||||||
const existing = await getToken().catch(() => null);
|
|
||||||
if (existing) {
|
|
||||||
return { content: [{ type: "text", text: "✓ Gültiges BEAM-Token bereits vorhanden. Kein Login nötig." }] };
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// kein Token — Login starten
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const token = await loginWithBrowser();
|
|
||||||
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
||||||
const expiry = new Date((payload.exp ?? 0) * 1000).toLocaleString("de-DE");
|
|
||||||
const user = payload.principal?.username ?? "Unbekannt";
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: `✓ BEAM Login erfolgreich!\nBenutzer: ${user}\nToken gültig bis: ${expiry}\nToken wird nur im Speicher gehalten.`,
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── Tool: beam_search_factsheets ──────────────────────────────────────────────
|
|
||||||
server.registerTool(
|
|
||||||
"beam_search_factsheets",
|
|
||||||
{
|
|
||||||
title: "BEAM Fact Sheets suchen",
|
|
||||||
description: `Sucht nach BEAM Fact Sheets anhand von Name, Beam-ID (z.B. 'A-106614') oder Freitext.
|
|
||||||
Gibt bis zu 20 Ergebnisse zurück mit Typ, Lebenszyklus und ID.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- query (string, required): Suchbegriff, Beam-ID oder Name
|
|
||||||
- type (enum, optional): Einschränkung auf Fact-Sheet-Typ
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Liste der gefundenen Fact Sheets mit Typ, Name, Lebenszyklus und UUID.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- query="A-106614" → Suche nach Beam-ID
|
|
||||||
- query="RiM", type="Application" → Anwendung nach Name suchen
|
|
||||||
- query="Personaleinsatz" → Freitextsuche über alle Typen`,
|
|
||||||
inputSchema: {
|
|
||||||
query: z.string()
|
|
||||||
.min(1, "Suchbegriff darf nicht leer sein")
|
|
||||||
.describe("Suchbegriff, Beam-ID (z.B. A-106614) oder Name"),
|
|
||||||
type: z.enum(["Application", "BusinessCapability", "ITComponent", "Interface", "DataObject"])
|
|
||||||
.optional()
|
|
||||||
.describe("Optional: Einschränkung auf einen Fact-Sheet-Typ"),
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
readOnlyHint: true,
|
|
||||||
destructiveHint: false,
|
|
||||||
idempotentHint: true,
|
|
||||||
openWorldHint: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ query, type }) => {
|
|
||||||
try {
|
|
||||||
const results = await searchFactSheets(query, type);
|
|
||||||
if (!results.length) {
|
|
||||||
return { content: [{ type: "text", text: `Keine Ergebnisse in BEAM für: ${query}` }] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = results.map((r: Record<string, unknown>) => {
|
|
||||||
const extId = (r.externalId as Record<string, string> | null)?.externalId ?? "";
|
|
||||||
const lc = lcLabel((r.lifecycle as Record<string, string> | null)?.asString);
|
|
||||||
const alias = r.alias ? ` (${r.alias})` : "";
|
|
||||||
return `• [${typeLabel(r.type as string)}] ${extId ? extId + " - " : ""}${r.name}${alias} | ${lc} | ID: ${r.id}`;
|
|
||||||
}).join("\n");
|
|
||||||
|
|
||||||
return { content: [{ type: "text", text: truncateIfNeeded(`BEAM Suchergebnisse für "${query}":\n\n${text}`) }] };
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// ── Tool: beam_get_factsheet ──────────────────────────────────────────────────
|
|
||||||
server.registerTool(
|
|
||||||
"beam_get_factsheet",
|
|
||||||
{
|
|
||||||
title: "BEAM Fact Sheet Details",
|
|
||||||
description: `Ruft vollständige Details eines BEAM Fact Sheets ab — per Beam-ID (z.B. 'A-106614') oder UUID.
|
|
||||||
Unterstützt Anwendungen, Domänen und IT-Komponenten mit Relationen.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- id (string, required): Beam-ID (z.B. A-106614, P.F.05.05.04) oder UUID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Detaillierte Fact-Sheet-Informationen inkl. Beschreibung, Lebenszyklus, Tags,
|
|
||||||
verknüpfte Domänen, IT-Komponenten und Nachfolger.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- id="A-106614" → Anwendung per Beam-ID
|
|
||||||
- id="P.F.05.05.04" → Domäne per externer ID
|
|
||||||
- id="33cb34f7-..." → Fact Sheet per UUID`,
|
|
||||||
inputSchema: {
|
|
||||||
id: z.string()
|
|
||||||
.min(1, "ID darf nicht leer sein")
|
|
||||||
.describe("Beam-ID (z.B. A-106614) oder UUID des Fact Sheets"),
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
readOnlyHint: true,
|
|
||||||
destructiveHint: false,
|
|
||||||
idempotentHint: true,
|
|
||||||
openWorldHint: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ id }) => {
|
|
||||||
try {
|
|
||||||
const fs = await getFactSheet(id) as Record<string, unknown>;
|
|
||||||
const lines: string[] = [];
|
|
||||||
|
|
||||||
const extId = (fs.externalId as Record<string, string> | null)?.externalId ?? "";
|
|
||||||
lines.push(`## ${extId ? extId + " - " : ""}${fs.name}`);
|
|
||||||
lines.push(`Typ: ${typeLabel(fs.type as string)}`);
|
|
||||||
lines.push(`Status: ${stateLabel(fs.lxState as string)}`);
|
|
||||||
|
|
||||||
if (fs.alias) lines.push(`Alias: ${fs.alias}`);
|
|
||||||
|
|
||||||
const lc = (fs.lifecycle as Record<string, string> | null)?.asString;
|
|
||||||
if (lc) lines.push(`Lebenszyklus: ${lcLabel(lc)}`);
|
|
||||||
|
|
||||||
if (fs.description) lines.push(`\nBeschreibung:\n${fs.description}`);
|
|
||||||
|
|
||||||
const tags = (fs.tags as { name: string }[] | null);
|
|
||||||
if (tags?.length) lines.push(`\nTags: ${tags.map(t => t.name).join(", ")}`);
|
|
||||||
|
|
||||||
// Domänen (für Anwendungen)
|
|
||||||
const domRel = fs.relApplicationToBusinessCapability as { edges: { node: Record<string, unknown> }[] } | null;
|
|
||||||
if (domRel?.edges?.length) {
|
|
||||||
lines.push("\n### Domänen");
|
|
||||||
domRel.edges.forEach(({ node }) => {
|
|
||||||
const dom = node.factSheet as Record<string, unknown>;
|
|
||||||
const domExtId = (dom.externalId as Record<string, string> | null)?.externalId ?? "";
|
|
||||||
lines.push(` • ${domExtId ? domExtId + " - " : ""}${dom.name} | Supporttyp: ${stLabel(node.supportType as string)}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anwendungen (für Domänen)
|
|
||||||
const appRel = fs.relBusinessCapabilityToApplication as { edges: { node: Record<string, unknown> }[] } | null;
|
|
||||||
if (appRel?.edges?.length) {
|
|
||||||
lines.push("\n### Anwendungen");
|
|
||||||
appRel.edges.forEach(({ node }) => {
|
|
||||||
const app = node.factSheet as Record<string, unknown>;
|
|
||||||
const appExtId = (app.externalId as Record<string, string> | null)?.externalId ?? "";
|
|
||||||
const appLc = lcLabel((app.lifecycle as Record<string, string> | null)?.asString);
|
|
||||||
lines.push(` • ${appExtId ? appExtId + " - " : ""}${app.name} | Supporttyp: ${stLabel(node.supportType as string)} | ${appLc}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// IT-Komponenten
|
|
||||||
const itcRel = fs.relApplicationToITComponent as { edges: { node: Record<string, unknown> }[] } | null;
|
|
||||||
if (itcRel?.edges?.length) {
|
|
||||||
lines.push("\n### IT-Komponenten");
|
|
||||||
itcRel.edges.forEach(({ node }) => {
|
|
||||||
lines.push(` • ${(node.factSheet as Record<string, unknown>).name}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nachfolger
|
|
||||||
const succRel = fs.relToSuccessor as { edges: { node: Record<string, unknown> }[] } | null;
|
|
||||||
if (succRel?.edges?.length) {
|
|
||||||
lines.push("\n### Nachfolger");
|
|
||||||
succRel.edges.forEach(({ node }) => {
|
|
||||||
const succ = node.factSheet as Record<string, unknown>;
|
|
||||||
lines.push(` • ${succ.name} (ID: ${succ.id})`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { content: [{ type: "text", text: truncateIfNeeded(lines.join("\n")) }] };
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// ── Tool: beam_get_domain_applications ─────────────────────────────────────────
|
|
||||||
server.registerTool(
|
|
||||||
"beam_get_domain_applications",
|
|
||||||
{
|
|
||||||
title: "BEAM Domänen-Anwendungen",
|
|
||||||
description: `Listet alle Anwendungen einer BEAM-Domäne auf, gruppiert nach Supporttyp (Führend/Unterstützt).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- domain (string, required): Domänen-ID (z.B. 'P.F.05.05.04'), Name oder UUID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Gruppierte Liste aller Anwendungen der Domäne mit Lebenszyklus und Tags.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- domain="P.F.05.05.04" → Domäne per externer ID
|
|
||||||
- domain="Informationen bereitstellen" → Domäne per Name
|
|
||||||
- domain="TO.I.01.01" → Technische Domäne`,
|
|
||||||
inputSchema: {
|
|
||||||
domain: z.string()
|
|
||||||
.min(1, "Domäne darf nicht leer sein")
|
|
||||||
.describe("Domänen-ID (z.B. 'P.F.05.05.04'), Name oder UUID"),
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
readOnlyHint: true,
|
|
||||||
destructiveHint: false,
|
|
||||||
idempotentHint: true,
|
|
||||||
openWorldHint: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ domain }) => {
|
|
||||||
try {
|
|
||||||
const fs = await getDomainApplications(domain) as Record<string, unknown>;
|
|
||||||
const extId = (fs.externalId as Record<string, string> | null)?.externalId ?? "";
|
|
||||||
|
|
||||||
const lines: string[] = [];
|
|
||||||
lines.push(`## BEAM-Domäne: ${extId ? extId + " - " : ""}${fs.name}`);
|
|
||||||
|
|
||||||
const appRel = fs.relBusinessCapabilityToApplication as { edges: { node: Record<string, unknown> }[] } | null;
|
|
||||||
if (!appRel?.edges?.length) {
|
|
||||||
lines.push("Keine Anwendungen in dieser Domäne gefunden.");
|
|
||||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push(`${appRel.edges.length} Anwendung(en)\n`);
|
|
||||||
|
|
||||||
const grouped: Record<string, string[]> = {};
|
|
||||||
appRel.edges.forEach(({ node }) => {
|
|
||||||
const app = node.factSheet as Record<string, unknown>;
|
|
||||||
const appExtId = (app.externalId as Record<string, string> | null)?.externalId ?? "";
|
|
||||||
const appLc = lcLabel((app.lifecycle as Record<string, string> | null)?.asString);
|
|
||||||
const st = stLabel(node.supportType as string);
|
|
||||||
const appTags = (app.tags as { name: string }[] | null)?.map(t => t.name).join(", ") ?? "";
|
|
||||||
const entry = ` • ${appExtId ? appExtId + " - " : ""}${app.name} | ${appLc}${appTags ? " | " + appTags : ""}`;
|
|
||||||
if (!grouped[st]) grouped[st] = [];
|
|
||||||
grouped[st].push(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
const order = ["Führend", "Unterstützt"];
|
|
||||||
const allTypes = [
|
|
||||||
...order.filter(k => grouped[k]),
|
|
||||||
...Object.keys(grouped).filter(k => !order.includes(k)),
|
|
||||||
];
|
|
||||||
|
|
||||||
allTypes.forEach((t) => {
|
|
||||||
lines.push(`### ${t} (${grouped[t].length})`);
|
|
||||||
grouped[t].forEach((e) => lines.push(e));
|
|
||||||
lines.push("");
|
|
||||||
});
|
|
||||||
|
|
||||||
return { content: [{ type: "text", text: truncateIfNeeded(lines.join("\n")) }] };
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// ── Tool: beam_get_diagram ────────────────────────────────────────────────────
|
|
||||||
server.registerTool(
|
|
||||||
"beam_get_diagram",
|
|
||||||
{
|
|
||||||
title: "BEAM Diagramme abrufen",
|
|
||||||
description: `Ruft Diagramme einer BEAM-Anwendung ab. Ohne diagram_name werden alle verknüpften Diagramme aufgelistet.
|
|
||||||
Mit diagram_name wird eine lesbare Zusammenfassung (Systeme, Schnittstellen) angezeigt.
|
|
||||||
Optional als .drawio-Datei speichern zum Öffnen in draw.io.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
- application (string, required): Anwendungsname, Beam-ID (z.B. 'A-109410') oder UUID
|
|
||||||
- diagram_name (string, optional): Name oder Teil des Diagrammnamens zum Abrufen
|
|
||||||
- save_path (string, optional): Dateipfad zum Speichern als .drawio-Datei
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Ohne diagram_name: Liste aller verfügbaren Diagramme.
|
|
||||||
Mit diagram_name: Lesbare Zusammenfassung mit Systemen/Akteuren und Schnittstellen.
|
|
||||||
Mit save_path: Zusätzlich wird eine .drawio-Datei gespeichert.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
- application="KEP_KEO_Revision" → Alle Diagramme auflisten
|
|
||||||
- application="A-109410", diagram_name="System Context" → Diagramm lesen
|
|
||||||
- application="A-109410", diagram_name="System Context", save_path="./diagram.drawio" → Als draw.io speichern`,
|
|
||||||
inputSchema: {
|
|
||||||
application: z.string()
|
|
||||||
.min(1, "Anwendung darf nicht leer sein")
|
|
||||||
.describe("Anwendungsname, Beam-ID (z.B. 'A-109410') oder UUID"),
|
|
||||||
diagram_name: z.string()
|
|
||||||
.optional()
|
|
||||||
.describe("Optional: Name oder Teil des Diagrammnamens zum Abrufen"),
|
|
||||||
save_path: z.string()
|
|
||||||
.optional()
|
|
||||||
.describe("Optional: Dateipfad zum Speichern als .drawio-Datei (z.B. './diagram.drawio')"),
|
|
||||||
},
|
|
||||||
annotations: {
|
|
||||||
readOnlyHint: false, // kann Dateien schreiben wenn save_path gesetzt
|
|
||||||
destructiveHint: false,
|
|
||||||
idempotentHint: true,
|
|
||||||
openWorldHint: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async ({ application, diagram_name, save_path }) => {
|
|
||||||
try {
|
|
||||||
const { factSheet, diagrams } = await getFactSheetDiagrams(application);
|
|
||||||
|
|
||||||
if (!diagrams.length) {
|
|
||||||
return { content: [{ type: "text", text: `Keine Diagramme bei ${factSheet.name} gefunden.` }] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nur auflisten wenn kein diagram_name
|
|
||||||
if (!diagram_name) {
|
|
||||||
const lines = [`## Diagramme von ${factSheet.name}\n`];
|
|
||||||
diagrams.forEach((d) => {
|
|
||||||
lines.push(` • ${d.name} [${d.groupKey}]\n ID: ${d.id}`);
|
|
||||||
});
|
|
||||||
lines.push(`\nNutze diagram_name um ein Diagramm abzurufen.`);
|
|
||||||
lines.push(`Nutze save_path um ein Diagramm als .drawio-Datei zu speichern.`);
|
|
||||||
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diagramm finden (Teilmatch)
|
|
||||||
const match = diagrams.find((d) =>
|
|
||||||
d.name.toLowerCase().includes(diagram_name.toLowerCase()) || d.id === diagram_name
|
|
||||||
);
|
|
||||||
if (!match) {
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: `Kein Diagramm gefunden für "${diagram_name}". Verfügbar:\n${diagrams.map((d) => ` • ${d.name}`).join("\n")}`,
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = await getDiagram(match.id);
|
|
||||||
const bm = raw.data;
|
|
||||||
|
|
||||||
const summary = parseDiagramToText(bm);
|
|
||||||
const contentParts: { type: "text"; text: string }[] = [{ type: "text", text: truncateIfNeeded(summary) }];
|
|
||||||
|
|
||||||
// Optional als .drawio speichern
|
|
||||||
const graphXml = extractGraphXml(bm);
|
|
||||||
if (save_path && graphXml) {
|
|
||||||
const filePath = saveDiagramAsDrawio(graphXml, (bm.name as string) ?? match.name, save_path);
|
|
||||||
contentParts.push({
|
|
||||||
type: "text",
|
|
||||||
text: `\n✓ Diagramm gespeichert: ${filePath}\nDie Datei kann direkt in draw.io geöffnet werden (Datei → Öffnen oder Doppelklick).`,
|
|
||||||
});
|
|
||||||
} else if (save_path && !graphXml) {
|
|
||||||
contentParts.push({
|
|
||||||
type: "text",
|
|
||||||
text: `\n⚠ Diagramm enthält kein graphXml — Speichern als .drawio nicht möglich.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { content: contentParts };
|
|
||||||
} catch (error) {
|
|
||||||
return handleError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── Start ─────────────────────────────────────────────────────────────────────
|
|
||||||
const transport = new StdioServerTransport();
|
|
||||||
await server.connect(transport);
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
/**
|
|
||||||
* Keychain-Modul: Sichere Speicherung von Credentials im OS-Keychain.
|
|
||||||
*
|
|
||||||
* - macOS: Keychain Access via `security` CLI
|
|
||||||
* - Windows: DPAPI-verschlüsselte Datei im AppData-Verzeichnis
|
|
||||||
* - Fallback: Kein persistenter Speicher (nur In-Memory)
|
|
||||||
*
|
|
||||||
* Keine externen Dependencies — nur Node.js Built-ins + OS-Tools.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
||||||
import { join } from "path";
|
|
||||||
import { homedir, platform } from "os";
|
|
||||||
|
|
||||||
const SERVICE_NAME = "beam-mcp";
|
|
||||||
const ACCOUNT_NAME = "beam-token";
|
|
||||||
|
|
||||||
// ── Platform Detection ────────────────────────────────────────────────────────
|
|
||||||
const OS = platform();
|
|
||||||
|
|
||||||
// ── macOS Keychain via `security` CLI ─────────────────────────────────────────
|
|
||||||
|
|
||||||
function macosGet(): string | null {
|
|
||||||
try {
|
|
||||||
const result = execSync(
|
|
||||||
`security find-generic-password -a "${ACCOUNT_NAME}" -s "${SERVICE_NAME}" -w`,
|
|
||||||
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
||||||
);
|
|
||||||
return result.trim() || null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function macosSet(token: string): void {
|
|
||||||
// -U flag updates existing entry or creates new one
|
|
||||||
execSync(
|
|
||||||
`security add-generic-password -a "${ACCOUNT_NAME}" -s "${SERVICE_NAME}" -w "${token.replace(/"/g, '\\"')}" -U`,
|
|
||||||
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function macosDelete(): void {
|
|
||||||
try {
|
|
||||||
execSync(
|
|
||||||
`security delete-generic-password -a "${ACCOUNT_NAME}" -s "${SERVICE_NAME}"`,
|
|
||||||
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// Ignore if entry doesn't exist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Windows DPAPI-encrypted file ──────────────────────────────────────────────
|
|
||||||
|
|
||||||
function getWindowsStorePath(): string {
|
|
||||||
const appData = process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local");
|
|
||||||
const dir = join(appData, "beam-mcp");
|
|
||||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
||||||
return join(dir, "credentials.enc");
|
|
||||||
}
|
|
||||||
|
|
||||||
function windowsGet(): string | null {
|
|
||||||
const storePath = getWindowsStorePath();
|
|
||||||
if (!existsSync(storePath)) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const encrypted = readFileSync(storePath, "utf8");
|
|
||||||
// Decrypt using DPAPI via PowerShell
|
|
||||||
const psScript = `
|
|
||||||
$bytes = [System.Convert]::FromBase64String('${encrypted}')
|
|
||||||
$decrypted = [System.Security.Cryptography.ProtectedData]::Unprotect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
||||||
[System.Text.Encoding]::UTF8.GetString($decrypted)
|
|
||||||
`;
|
|
||||||
const result = execSync(
|
|
||||||
`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, " ")}"`,
|
|
||||||
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
||||||
);
|
|
||||||
return result.trim() || null;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function windowsSet(token: string): void {
|
|
||||||
const storePath = getWindowsStorePath();
|
|
||||||
// Encrypt using DPAPI via PowerShell
|
|
||||||
const psScript = `
|
|
||||||
Add-Type -AssemblyName System.Security
|
|
||||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes('${token.replace(/'/g, "''")}')
|
|
||||||
$encrypted = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)
|
|
||||||
[System.Convert]::ToBase64String($encrypted)
|
|
||||||
`;
|
|
||||||
const result = execSync(
|
|
||||||
`powershell -NoProfile -NonInteractive -Command "${psScript.replace(/\n/g, " ")}"`,
|
|
||||||
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
|
|
||||||
);
|
|
||||||
writeFileSync(storePath, result.trim(), "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
function windowsDelete(): void {
|
|
||||||
const storePath = getWindowsStorePath();
|
|
||||||
try {
|
|
||||||
if (existsSync(storePath)) unlinkSync(storePath);
|
|
||||||
} catch {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Public API ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liest das gespeicherte Token aus dem OS-Keychain.
|
|
||||||
* Gibt null zurück wenn kein Token gespeichert ist oder das OS nicht unterstützt wird.
|
|
||||||
*/
|
|
||||||
export function getStoredToken(): string | null {
|
|
||||||
switch (OS) {
|
|
||||||
case "darwin":
|
|
||||||
return macosGet();
|
|
||||||
case "win32":
|
|
||||||
return windowsGet();
|
|
||||||
default:
|
|
||||||
process.stderr.write(`[BEAM] Keychain nicht unterstützt auf ${OS}. Nur In-Memory Token.\n`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Speichert ein Token sicher im OS-Keychain.
|
|
||||||
* Überschreibt ein vorhandenes Token.
|
|
||||||
*/
|
|
||||||
export function storeToken(token: string): void {
|
|
||||||
switch (OS) {
|
|
||||||
case "darwin":
|
|
||||||
macosSet(token);
|
|
||||||
process.stderr.write("[BEAM] Token in macOS Keychain gespeichert.\n");
|
|
||||||
break;
|
|
||||||
case "win32":
|
|
||||||
windowsSet(token);
|
|
||||||
process.stderr.write("[BEAM] Token verschlüsselt gespeichert (Windows DPAPI).\n");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
process.stderr.write(`[BEAM] Keychain nicht unterstützt auf ${OS}. Token nur im Speicher.\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Löscht das gespeicherte Token aus dem OS-Keychain.
|
|
||||||
*/
|
|
||||||
export function deleteStoredToken(): void {
|
|
||||||
switch (OS) {
|
|
||||||
case "darwin":
|
|
||||||
macosDelete();
|
|
||||||
break;
|
|
||||||
case "win32":
|
|
||||||
windowsDelete();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob Keychain-Unterstützung auf diesem OS verfügbar ist.
|
|
||||||
*/
|
|
||||||
export function isKeychainSupported(): boolean {
|
|
||||||
return OS === "darwin" || OS === "win32";
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Standalone Login-Skript
|
|
||||||
* Öffnet einen sichtbaren Edge-Browser, führt den BEAM-Login durch.
|
|
||||||
* Das Token wird im OS-Keychain gespeichert (macOS Keychain / Windows DPAPI).
|
|
||||||
*
|
|
||||||
* Verwendung: npm run login
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { loginWithBrowser } from "./auth.js";
|
|
||||||
import { isKeychainSupported } from "./keychain.js";
|
|
||||||
|
|
||||||
console.log("🔐 BEAM Login wird gestartet...");
|
|
||||||
console.log(" Ein Edge-Browser-Fenster öffnet sich gleich.\n");
|
|
||||||
|
|
||||||
if (!isKeychainSupported()) {
|
|
||||||
console.warn("⚠ Keychain wird auf diesem OS nicht unterstützt.");
|
|
||||||
console.warn(" Token wird nur im Speicher gehalten (nicht persistent).\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const token = await loginWithBrowser();
|
|
||||||
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
||||||
const expiry = new Date((payload.exp ?? 0) * 1000).toLocaleString("de-DE");
|
|
||||||
const user = payload.principal?.username ?? payload.sub ?? "Unbekannt";
|
|
||||||
|
|
||||||
console.log("✅ Login erfolgreich!");
|
|
||||||
console.log(` Benutzer: ${user}`);
|
|
||||||
console.log(` Token gültig bis: ${expiry}`);
|
|
||||||
console.log(` Gespeichert in: OS-Keychain (${process.platform === "darwin" ? "macOS Keychain" : "Windows DPAPI"})`);
|
|
||||||
console.log("\n Der MCP-Server nutzt das Token automatisch beim nächsten Start.");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Login fehlgeschlagen:", err instanceof Error ? err.message : err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2022",
|
|
||||||
"module": "Node16",
|
|
||||||
"moduleResolution": "Node16",
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user