Skip to main content
Glama
matej2314

Unified Agent Context System

by matej2314
README.md
# Unified Agent Context System (MCP)

Globalny serwer [Model Context Protocol](https://modelcontextprotocol.io/) z rozdzieleniem rdzenia, transportu i modułów narzędzi per domena.

## Architektura

- **Core** (`src/core/`) — tworzenie instancji `McpServer`, typy wspólne dla modułów, `ToolRegistry` ładujący moduły z konfiguracji.
- **Transports** (`src/transports/`) — adaptery MCP: **Streamable HTTP** ([`http.ts`](./src/transports/http.ts), sesje, Express przez SDK) oraz **stdio** ([`stdio.ts`](./src/transports/stdio.ts)) — w [`src/index.ts`](./src/index.ts) domyślnie startuje **HTTP**; stdio włączasz ręcznie w entry point (krok po kroku: [Transport stdio (aktywacja)](#transport-stdio-aktywacja-alternatywy-do-http)).
- **Observability** (`src/observability/`) — health checks oraz metryki Prometheus; endpointy podpinane w transporcie HTTP (szczegóły: [Observability](#observability)).
- **Modules** (`src/modules/<nazwa>/`) — każdy moduł eksportuje `register(server, options)` i rejestruje narzędzia pod własnym namespace z env / `config/modules.config.ts`.
- **Config** (`config/modules.config.ts`) — lista modułów, flagi `ENABLE_MODULE_*`, namespace i opcje (np. `contentRoot` dla portfolio).

Przepływ (obecny domyślny): `src/index.ts` → `createMcpServer()` → `ToolRegistry.loadModules()` → `startHttpTransport(buildMcpServer, { port, host })` (Bearer: **wymagany** niepusty `MCP_INTERNAL_TOKEN` — `http.ts` odrzuca start bez niego i weryfikuje nagłówek na każdym żądaniu).

## Moduły

### Portfolio

Treść z plików Markdown i `manifest.json` w katalogu content (read-only, ścieżki przez `safeJoin` z ochroną przed path traversal).

**Lib:** `corpus.ts`, `paths.ts`, `frontmatter.ts`, `search.ts`, `filterHelpers.ts`, `toolResponse.ts`, `validSections.ts`, `toolManifestData.ts`.

**Narzędzia** (prefix domyślny `portfolio_`, konfigurowalny przez `PORTFOLIO_NAMESPACE`):

| Narzędzie | Opis |
|-----------|------|
| `{ns}_get_profile` | Profil publiczny |
| `{ns}_get_about` | Sekcja „O mnie” |
| `{ns}_get_manifest` | `manifest.json` (sekcje, metadane MVP) |
| `{ns}_search` | Wyszukiwanie po korpusie |
| `{ns}_projects_query`, `{ns}_projects_list`, `{ns}_projects_get`, `{ns}_projects_tags` | Projekty |
| `{ns}_skills_query`, `{ns}_skills_list`, `{ns}_skills_get`, `{ns}_skills_tags`, `{ns}_skills_categories` | Umiejętności |
| `{ns}_experience_query`, `{ns}_experience_list`, `{ns}_experience_get`, `{ns}_experience_tags` | Doświadczenie |
| `{ns}_courses_query`, `{ns}_courses_list`, `{ns}_courses_get`, `{ns}_courses_tags`, `{ns}_courses_categories`, `{ns}_courses_platforms` | Kursy |

`{ns}` = wartość namespace (np. `portfolio`).

### test-tools

Przykładowy moduł demonstracyjny (`zod` + `server.tool`): `{ns}_get_user_data`, `{ns}_get_products`. Domyślny namespace: `test` (`TEST_TOOLS_NAMESPACE`). W środowisku produkcyjnym zwykle wyłączasz: `ENABLE_MODULE_TEST_TOOLS=false`.

Moduł jest **włączony domyślnie** (jak portfolio), dopóki nie ustawisz `ENABLE_MODULE_*=false`.

## Struktura repozytorium

```
.                                 # korzeń repozytorium (nazwa katalogu zależy od klonu)
├── config/
│   └── modules.config.ts          # Moduły, namespace, contentRoot, corpusVersion
├── scripts/
│   └── copy-portfolio-content.mjs # Po tsc: kopiuje content → dist/.../portfolio/content
├── Dockerfile
├── docker-compose.yml             # Przykładowe wdrożenie HTTP (MCP_HOST, MCP_PORT)
├── src/
│   ├── index.ts                   # Entry: server → registry → HTTP (domyślnie)
│   ├── core/
│   │   ├── server.ts
│   │   ├── toolRegistry.ts
│   │   └── types.ts
│   ├── transports/
│   │   ├── http.ts                # Streamable HTTP, /mcp, Bearer; podpina /healthz i /metrics
│   │   └── stdio.ts
│   ├── observability/
│   │   ├── health.ts              # Aggregacja checkHealth włączonych modułów
│   │   ├── metrics.ts             # prom-client: HTTP, sesje, auth, toolsy
│   │   └── instrumentTool.ts      # Wrapper registerTool + observeTool
│   └── modules/
│       ├── portfolio/
│       │   ├── index.ts           # register*: profile, about, manifest, search, projects, skills, experience, courses
│       │   ├── content/           # .md, manifest.json (+ kopia w dist po buildzie)
│       │   ├── lib/
│       │   └── tools/
│       └── test-tools/
│           ├── index.ts
│           └── tools/
└── dist/                          # `tsc` → m.in. dist/src/index.js
```

## Instalacja

```bash
npm install
```

## Konfiguracja

```bash
cp .env.example .env
```

Ważne zmienne (szczegóły w [`.env.example`](./.env.example)):

- **Portfolio:** `ENABLE_MODULE_PORTFOLIO`, `PORTFOLIO_NAMESPACE`, `PORTFOLIO_CONTENT_ROOT`, `PORTFOLIO_CORPUS_VERSION`
- **test-tools:** `ENABLE_MODULE_TEST_TOOLS`, `TEST_TOOLS_NAMESPACE`
- **Transport HTTP (aktywny w `src/index.ts`):** `MCP_HOST` (domyślnie `127.0.0.1`), `MCP_PORT` (domyślnie `3333`), **`MCP_INTERNAL_TOKEN`** — **wymagana** niepusta wartość; bez niej proces nie wystartuje; każde żądanie HTTP musi mieć `Authorization: Bearer <ten sam token>`
- **Observability (HTTP):** `HEALTH_ENABLED` / `METRICS_ENABLED` (domyślnie włączone; wyłączenie: `false`), `HEALTH_PATH` (domyślnie `/healthz`), `METRICS_PATH` (domyślnie `/metrics`)

## Observability

Warstwa w [`src/observability/`](./src/observability/) — health i metryki Prometheus. Endpointy rejestruje wyłącznie **transport HTTP** w [`src/transports/http.ts`](./src/transports/http.ts) (przy stdio brak `/healthz` i `/metrics`). Ścieżki `/healthz` i `/metrics` **nie** wymagają Bearer (middleware auth dotyczy tylko ścieżek pod mount MCP, domyślnie `/mcp`).

### Health (`health.ts`)

- Włączane, gdy `HEALTH_ENABLED !== 'false'` (ścieżka: `HEALTH_PATH`, domyślnie `/healthz`).
- `GET` zwraca JSON `{ status, checks }` — HTTP **200** przy `status: "ok"`, **503** przy `"degraded"`.
- `buildHealthPayload()` przechodzi włączone wpisy z `modules.config`, dynamicznie ładuje moduł i — jeśli eksportuje opcjonalne `checkHealth` — zbiera wyniki typu `HealthCheck` (`id`, `ok`, `detail?` z [`src/core/types.ts`](./src/core/types.ts)).
- Status globalny: `"ok"`, gdy wszystkie checki mają `ok: true`; inaczej `"degraded"`. Wyjątek w checkerze staje się checkiem z `ok: false`.
- **Portfolio** eksportuje `checkHealth`: weryfikuje, że `contentRoot` jest ustawiony i czytelny (`portfolio_content_root`).

### Metryki Prometheus (`metrics.ts` + `instrumentTool.ts`)

- Biblioteka: [`prom-client`](https://github.com/siimon/prom-client). Włączane, gdy `METRICS_ENABLED !== 'false'` (ścieżka: `METRICS_PATH`, domyślnie `/metrics`).
- Przy starcie HTTP: `initMetrics()` — domyślne metryki procesu z prefixem `mcp_` oraz gauge `mcp_build_info{version="1.0.0"}`.
- Endpoint ekspozycji: `Content-Type` z `prom-client`, body z `register.metrics()`.

| Metryka | Typ | Etykiety / sens |
|---------|-----|-----------------|
| `mcp_http_requests_total` | Counter | `method`, `status_class` (`1xx`…`5xx`), `module` |
| `mcp_auth_failures_total` | Counter | `reason`: `missing_token` \| `invalid_token` |
| `mcp_protocol_errors_total` | Counter | `module`, `code` (np. `missing_module`, `unknown_module`, `session_mismatch`, `invalid_session`, …) |
| `mcp_sessions_active` | Gauge | `module` — aktywne sesje |
| `mcp_sessions_opened_total` / `mcp_sessions_closed_total` | Counter | `module` |
| `mcp_tool_invocations_total` | Counter | `module`, `tool`, `result` (`ok` \| `error`) |
| `mcp_tool_duration_seconds` | Histogram | `module`, `tool` — czas handlera |

Transport HTTP zapisuje m.in. requesty (po `res.finish`), błędy auth/protokołu oraz lifecycle sesji. Narzędzia portfolio rejestrują się przez **`registerInstrumentedTool`**: owija handler, mierzy czas (`hrtime`) i woła `observeTool` (sukces / `isError` / wyjątek).

### Request ID

Każde żądanie HTTP dostaje `requestId` z nagłówka `x-request-id` (jeśli niepusty) albo świeże `randomUUID()` — pole na obiekcie requestu w Express.

## Uruchomienie

### Rozwój (watch)

```bash
npm run dev
```

### Produkcja

```bash
npm run build
npm start
```

`build` uruchamia `tsc` oraz `scripts/copy-portfolio-content.mjs`, żeby przy `npm start` (`node dist/src/index.js`) katalog `content` modułu portfolio był dostępny obok skompilowanych plików (domyślny `PORTFOLIO_CONTENT_ROOT` względem `lib` w `dist`).

## Transport stdio (aktywacja alternatywy do HTTP)

Alternatywny transport MCP znajduje się w [`src/transports/stdio.ts`](./src/transports/stdio.ts): funkcja **`startStdioTransport(server)`** podłącza jedną instancję `McpServer` do **`StdioServerTransport`** (komunikacja JSON-RPC MCP wyłącznie przez **stdin / stdout**). Nadaje się do klientów, które **spawnują** proces Node i nie otwierają portu HTTP (typowo **Cursor** z konfiguracją `command` + `args`).

**Jak włączyć stdio** (ręczna zmiana w [`src/index.ts`](./src/index.ts), potem `npm run build`):

1. **Import:** odkomentuj `import { startStdioTransport } from './transports/stdio.js';` i zakomentuj (lub usuń) import `startHttpTransport`, jeśli nie jest już potrzebny.
2. **Funkcja `main()`:** usuń lub zakomentuj cały obecny blok: **sonda startowa** (`probe` z `createServerWithModules` + `probe.close()`), a także **`startHttpTransport(...)`**. Sonda zamyka pierwszą instancję serwera — przed stdio **nie** powinna zostawiać zamkniętego serwera jako jedynej ścieżki.
3. **Zastąp** logiką w stylu poniżej — **jedna** instancja serwera na proces, bez nasłuchu TCP:

```typescript
const { server, loadedModules } = await createServerWithModules();
console.error(`[Server] Loaded ${loadedModules.length} modules: ${loadedModules.join(', ')}`);
await startStdioTransport(server);
```

4. W repozytorium w komentarzu jest skrócona wersja tej samej idei (linia przy `// Stdio (jeden proces, jedna sesja):`); możesz ją wkleić zamiast ręcznego składania bloku — byle **nie** mieszać jednocześnie z `startHttpTransport` w tym samym `main()`.

**Zmienne `MCP_HOST` / `MCP_PORT` / `MCP_INTERNAL_TOKEN`** nie dotyczą trybu stdio (brak serwera HTTP). Po przełączeniu z powrotem na HTTP przywróć import i wywołanie `startHttpTransport` oraz (opcjonalnie) sondę diagnostyczną, jeśli z niej korzystasz.

## Integracja z Cursor IDE

**Uwaga:** domyślny `src/index.ts` uruchamia **serwer HTTP**, a nie MCP po **stdio**. Konfiguracja Cursor z polami `command` + `args` zakłada zwykle **stdio**. Żeby ta ścieżka działała, najpierw **aktywuj stdio** według sekcji [Transport stdio (aktywacja alternatywy do HTTP)](#transport-stdio-aktywacja-alternatywy-do-http) (albo wydziel osobny plik startowy tylko pod stdio, żeby nie przełączać `index.ts` przy każdej zmianie środowiska).

Gdy masz już tryb **stdio** w procesie uruchamianym przez Cursor:

1. Zbuduj projekt: `npm run build`
2. W konfiguracji MCP ustaw serwer uruchamiany przez Node ze ścieżką do **skompilowanego** entry point (ten sam plik co dziś, ale po przełączeniu na stdio w kodzie):

```json
{
  "mcpServers": {
    "unified_agent_context_mcp": {
      "command": "node",
      "args": [
        "C:\\Users\\matej\\Desktop\\projekt JS\\custom_mcp_server\\dist\\src\\index.js"
      ],
      "env": {
        "ENABLE_MODULE_PORTFOLIO": "true",
        "ENABLE_MODULE_TEST_TOOLS": "true",
        "PORTFOLIO_NAMESPACE": "portfolio",
        "PORTFOLIO_CONTENT_ROOT": "C:\\Users\\matej\\Desktop\\projekt JS\\custom_mcp_server\\dist\\src\\modules\\portfolio\\content"
      }
    }
  }
}
```

Klucz `unified_agent_context_mcp` w JSON jest **dowolny** (to etykieta w Cursorze); możesz trzymać np. `mateo2314_mcp_server`, jeśli masz już starą konfigurację.

Dostosuj ścieżki do swojego dysku. Dla samego portfolio możesz wskazać `src\\...\\content` przy `tsx`/dev; przy `npm start` wygodniej jest katalog w `dist` po skrypcie kopiującym.

3. Zrestartuj Cursor i sprawdź panel MCP.

**HTTP (VPS / agenci zdalni):** po `npm start` endpoint MCP jest pod ścieżką **`/mcp`** (Streamable HTTP). Ustaw `MCP_INTERNAL_TOKEN` w środowisku (np. `.env` / `docker-compose`) i dołącz do każdego żądania nagłówek `Authorization: Bearer …` z tą wartością. W repozytorium jest przykład portów i `MCP_HOST=0.0.0.0` w [`docker-compose.yml`](./docker-compose.yml).

## Testowanie w Cursor

```
Jakie narzędzia MCP są dostępne?
```

```
Użyj portfolio_get_profile i pokaż wynik
```

```
Wyszukaj "TypeScript" przez portfolio_search
```

## Treść portfolio

Pliki w `src/modules/portfolio/content/` (po buildzie także pod `dist/.../content/`). Struktura zależy od Twojego korpusu; obowiązuje m.in. `manifest.json` zgodny z narzędziem `get_manifest`.

Przykład front matter w `.md`:

```markdown
---
project_name: Mój projekt
project_category: Web Development
tech_stack: Next.js, TypeScript
---

Opis...
```

## Migracja / rozszerzenia

### Baza zamiast plików

Zachowaj nazwy narzędzi i kontrakty; podmień implementację w warstwie lib (np. `corpus.ts`) na odczyt z DB. Zmienne typu `PORTFOLIO_DB_URL` są przygotowane w `.env.example` jako szkic.

### Transport HTTP

Zaimplementowany w [`src/transports/http.ts`](./src/transports/http.ts) (Streamable HTTP, sesje, **wymagany** Bearer `MCP_INTERNAL_TOKEN`). Szerszy opis planu / decyzji mógł być w `MCP.md` — plik może być wyłączony z gita (patrz `.gitignore`); stan faktyczny zawsze w kodzie `index.ts` + `http.ts`.

### Transport stdio

Implementacja: [`src/transports/stdio.ts`](./src/transports/stdio.ts). Aktywacja w entry point: [Transport stdio (aktywacja alternatywy do HTTP)](#transport-stdio-aktywacja-alternatywy-do-http).

## Dodawanie modułu

1. Katalog `src/modules/<nazwa>/` z `index.ts` i `export async function register(server, options)`.
2. Wpis w `config/modules.config.ts` (nazwa = segment ścieżki importu `../modules/${name}/index.js`).
3. Zmienne `ENABLE_MODULE_<NAZWA>` / namespace w `.env` według konwencji projektu.

## Dokumentacja

- [MCP_MVP.md](./MCP_MVP.md) — plan MVP
- [MCP.md](./MCP.md) — architektura globalnego serwera i transporty
- [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)

## Troubleshooting

### Brak połączenia z klientem MCP

- Logi: Output → MCP
- Ścieżki w Windows: poprawne escapowanie `\\` w JSON
- Czy istnieje `dist/src/index.js` po `npm run build`
- **Cursor + `command`/`args`:** czy proces naprawdę używa **stdio**? Domyślny `index.js` startuje **HTTP** — wtedy Cursor nie „zobaczy” MCP po stdin/stdout; przełącz transport w kodzie albo osobny entry pod stdio

### Brak narzędzi / pusty moduł

- W logach powinna być linia w stylu: `Loaded N modules: portfolio, test-tools`
- `ENABLE_MODULE_PORTFOLIO=false` wyłącza portfolio; to samo dla `test-tools`
- Portfolio: czy `content` istnieje (src lub dist + skrypt kopiujący)

### Path traversal detected

Zamierzone dla ścieżek wychodzących poza `PORTFOLIO_CONTENT_ROOT`. Sprawdź `PORTFOLIO_CONTENT_ROOT` i argumenty narzędzi.

## Licencja

ISC