Skip to main content
Glama
LanaKee

gyeongbuk-public-ai

by LanaKee

G-BAP (Gyeongbuk Behind-the-scenes Access Platform)

A hackathon vertical slice that queries Gyeongsangbuk-do and its cities/counties population, local tax, policy, and council data via purpose-built MCP tools, presenting conclusions, evidence, sources, and limitations together.

[!IMPORTANT] Operational data and basic queries use only official_observed. When using results, verify the original source and reference date. demo_sample operational rows and automatic fallback have been removed.

Implementation Scope

  • Responsive Korean web UI with 4 representative questions

  • MCP server based on the official @modelcontextprotocol/sdk (local stdio + Streamable HTTP for hosting)

  • HTTP API and demo orchestration calling the same tools

  • 10 tools: population and local tax trends, cross-comparison, policy eligibility explanations, council search, press release search, per-councilmember bill search, claim verification, source lookup, and evidence-constrained administrative document drafting

  • CP949/UTF-8 CSV and XLSX ETL that identifies files by header/content rather than relying on NFD filenames

  • KRW unit normalization, quality validation, isolation, and official-first querying across 9 local tax source grains and the Ministry of the Interior and Safety HWPX yearbook

  • SHA-256 catalog generation for all provided CSV, PDF, and XLSX files

  • Crawl of 13,707 Gyeongsangbuk-do press releases (2020~2026) with a search tool dedicated to descriptive context

  • Crawl of 64 Gyeongsangbuk-do Council members, 1,454 proposed bills, and 24,178 sponsor relationships, with a per-councilmember bill search tool separating lead/co-sponsorship

  • PDF.js-based page extraction, chunking, and local embedding loading for 408 meeting minutes, audits, and provincial administration work plan PDFs

  • Page-level Korean plausibility/mojibake metrics, repeated header removal, speaker/table boundary chunking, and golden regression evaluation

  • Lifecycle that searches only approved PDF processing versions, purge based on retention period and confirmation tokens, and capacity reporting

  • PostgreSQL normalized schema, migrations, and Docker Compose

  • PostgreSQL hybrid repository combining Korean FTS/trigram with stored embeddings

  • Feature-flag-based PostgreSQL MCP repository, readiness, and redacted tool audit logs

  • Vitest-based domain, search, ETL, tool, and HTTP tests

Related MCP server: Korea Stats MCP

Benchmark: The Difference an Evidence Layer Actually Makes

To answer "why not just ask ChatGPT/RAG?", we measured the same model, same question with only the data-layer access toggled. Scoring is done purely with deterministic code—no LLM judge—and ground truth is queried directly from PostgreSQL at execution time. The full methodology is in benchmark/README.md.

1) Vanilla LLM vs MCP(G-BAP) — 22 questions · 6 categories, 3 models

Model

Accuracy (vanilla → MCP)

Hallucination rate per answer (vanilla → MCP)

Verifiable source rate (vanilla → MCP)

claude-sonnet-5

50.0% → 81.8% (+31.8p)

50.0% → 16.7%

0.0% → 80.0%

gpt-4o-mini

27.3% → 72.7% (+45.5p)

33.3% → 22.2%

0.0% → 56.8%

gpt-5.6-luna

36.4% → 81.8% (+45.5p)

63.6% → 11.8%

0.0% → 65.9%

  • Accuracy is higher in the MCP arm across all 3 models with valid runs.

  • The vanilla verifiable source rate is 0% for every model—not because no sources are cited, but because the cited sources do not correspond to real rows.

  • In the abstention category (questions where data genuinely does not exist), the correct answer is to not answer, and the MCP arm achieves 100% absence-recognition accuracy across all three models.

  • Full results: benchmark/reports/COMPARISON.md.

2) Raw RAG vs Normalized Data Layer(G-BAP) — 10-question discrimination experiment

We compared, on the same model (gpt-5.6-luna), an arm fed 446 raw files and 258K chunks via simple lexical retrieval versus an arm using G-BAP tools with normalization, metadata, provenance, and coverage.

Metric

Raw Retrieval (RAG)

G-BAP (structured layer)

Accuracy

60.0%

90.0%

Answer attempt rate

50.0%

70.0%

Hallucination rate (per answer)

40.0%

14.3%

Absence-recognition accuracy

100.0%

100.0%

Verifiable source rate

0.0%

56.3%

Feeding raw documents directly into search gets you partway there (60%), but a layer with unit normalization, quality validation, and source tables raises accuracy to 90% and cuts hallucinations to a third or less. Full results: raw-rag-vs-mcp-gpt-5.6-luna-gyeongbuk-v2/report.md.

Reproduction

pnpm benchmark:compare:dry-run   # API 호출 없이 채점기·문항 전제만 검증
pnpm benchmark:compare           # vanilla vs MCP 전체 비교
pnpm benchmark:raw-rag           # 무가공 RAG vs 구조화 계층 비교

Some models cannot fix temperature across the 22 questions, so the sample is not large. This is not a cross-model absolute ranking; it measures each model's internal vanilla → MCP delta. The common_knowledge category (control) is deliberately designed so vanilla can win.

Quick Start

Requirements: Node.js 22+, pnpm 10+

pnpm install
pnpm test
pnpm build
pnpm start

Open http://localhost:8787 in a browser.

The UI is the Next.js app in prototype/ and runs on 3100. Port 8787 proxies everything except its own paths (/mcp, /health, /ready, /api/*) to it. pnpm start and pnpm dev launch both processes together, so nothing needs to be run separately. To run only the server without the UI, use SKIP_PROTOTYPE=1; the UI paths then return 404. To reuse an already-running UI, pass its address in PROTOTYPE_ORIGIN.

Command

What it does

pnpm start

UI (3100) + MCP server (8787)

pnpm dev

Same setup in development mode

pnpm start:server

MCP server only

SKIP_PROTOTYPE=1 pnpm start

MCP server only (UI paths 404)

DATA_REPOSITORY=auto is the default, and startup fails if DATABASE_URL is missing or PostgreSQL is not ready. Explicit memory mode starts with an empty repository. .env is read automatically by the server. The default local HTTP auth is HTTP_AUTH_MODE=off and must not be exposed externally. The server bind default is also HTTP_HOST=127.0.0.1; only Docker explicitly uses 0.0.0.0.

Development mode:

pnpm dev

Docker

HTTP_AUTH_TOKEN="$(openssl rand -hex 32)" docker compose up --build

Compose enforces HTTP_AUTH_MODE=token and will not start without a token. Production deployments should sit behind a reverse proxy terminating TLS, with the token injected via a secret manager or deployment secret. Do not commit tokens to the repository.

To apply migrations to an existing PostgreSQL:

pnpm db:schema

DATABASE_URL is read from the environment or from .env in the repository; the command output never exposes the connection string.

MCP stdio Configuration

First run pnpm install && pnpm build, then register with your MCP client as follows.

{
  "mcpServers": {
    "g-bap": {
      "command": "node",
      "args": [
        "/absolute/path/to/g-bap/services/mcp-server/dist/index.js",
        "--stdio"
      ]
    }
  }
}

During development you can run the stdio server directly with:

pnpm --filter @gb/mcp-server dev:stdio

The server uses stdout exclusively for JSON-RPC. HTTP authentication and rate-limit settings do not apply to the local stdio transport.

Remote MCP Hosting (Streamable HTTP)

Running in HTTP mode serves the MCP standard Streamable HTTP transport at /mcp. It exposes exactly the same tool list as stdio.

HTTP_HOST=0.0.0.0 \
PORT=8787 \
HTTP_AUTH_MODE=token \
HTTP_AUTH_TOKEN=<32바이트 이상 토큰> \
DATABASE_URL=postgresql://... \
DATA_REPOSITORY=postgres \
node services/mcp-server/dist/index.js --http

Remote MCP client registration:

{
  "mcpServers": {
    "g-bap": {
      "type": "http",
      "url": "https://<host>/mcp",
      "headers": { "Authorization": "Bearer <HTTP_AUTH_TOKEN>" }
    }
  }
}

Variable

Default

Description

MCP_MAX_SESSIONS

256

Concurrent session cap. Returns 503 when exceeded

MCP_SESSION_IDLE_MS

600000

Idle session reclamation threshold

MCP_ALLOWED_HOSTS

(empty)

Host allow-list for DNS rebinding defense

MCP_ALLOWED_ORIGINS

(empty)

Same-origin allow-list

  • On public hosting, HTTP_AUTH_MODE=token is mandatory. Failed authentication does not create a session and disconnects with 401.

  • If both MCP_ALLOWED_HOSTS and MCP_ALLOWED_ORIGINS are empty, DNS rebinding defense is off. Set them unless you are behind a trusted proxy.

  • Protocol traffic on /mcp consumes the cheap budget; expensive tools within tools/call additionally consume the expensive budget. See docs/CHANGES_HOSTED_MCP_SERVER.md for details.

HTTP API

Method

Path

Description

GET

/health

Process liveness check

GET

/ready

Exposes only minimal ready/not-ready status

GET

/api/questions

Representative question list

GET

/api/admin/storage

Token-only DB capacity, lifecycle, and safe settings summary

POST

/api/agent

Accepts a natural-language question and performs dataset exploration, tool selection, and evidence collection

POST

/api/tools/:toolName

Executes the same tool functions as MCP

curl http://localhost:8787/health

export HTTP_AUTH_TOKEN="$(openssl rand -hex 32)"
HTTP_AUTH_MODE=token pnpm start

curl -X POST http://localhost:8787/api/agent \
  -H "authorization: Bearer $HTTP_AUTH_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"question":"포항시는 2018년부터 2024년까지 인구는 줄었는데 지방세 세수는 늘었나?"}'

curl -X POST http://localhost:8787/api/tools/search_policies \
  -H "authorization: Bearer $HTTP_AUTH_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"region":"상주시","age":34,"query":"스마트팜","stage":"예비창업"}'

curl -X POST http://localhost:8787/api/tools/search_council_records \
  -H "authorization: Bearer $HTTP_AUTH_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"query":"지방소멸","dateFrom":"2020-01-01","dateTo":"2024-12-31","limit":5}'

# 보도자료는 서술적 맥락 전용이라 dataStatuses를 받지 않는다
curl -X POST http://localhost:8787/api/tools/search_press_releases \
  -H "authorization: ******" \
  -H 'content-type: application/json' \
  -d '{"query":"청년 일자리","dateFrom":"2024-01-01","limit":5}'

# 의원별 의안. 대표발의(lead)와 공동발의(co)는 절대 합산하지 않는다
curl -X POST http://localhost:8787/api/tools/search_member_bills \
  -H "authorization: ******" \
  -H 'content-type: application/json' \
  -d '{"member":"김대진","query":"산림","role":"lead","limit":5}'

Every factual response includes top-level data_status, data_statuses, sources, and limitations. The input's optional dataStatuses can specify official_observed/demo_sample, but the default allows official data only. If more than one status is actually returned, data_status is "mixed" and individual statuses are preserved in data_statuses and per-record. Derived statistics are not considered safe based on label alone. summary is computed only when two or more points share the same dataStatus, accounting basis, amount semantics, source grain, and raw unit, and for population also the same measurement basis and reference-date precision. Otherwise, the original points and homogeneous series_segments are preserved while returning summary: null and summary_unavailable_reason. Population/local-tax match verdicts are generated only when both sides are comparable official series.

HTTP Access Control

  • HTTP_AUTH_MODE=token requires Authorization: Bearer or X-API-Key on /api/**. Tokens must be at least 32 bytes; weak placeholders are rejected at startup.

  • /health, the minimal-information /ready, and the static UI are public. In token mode, the public UI does not put the API token in the browser, so API-calling features do not work without reverse-proxy auth integration.

  • Token comparison uses constant-time comparison of SHA-256 fixed-length digests; tokens/raw bodies are never stored in logs or audit tables.

  • Per-client in-memory token buckets default to 120 cheap requests/min, 12 expensive (PDF hybrid/demo/draft, etc.), and 60 readiness. Exceeding returns 429 with Retry-After.

  • The client is the socket IP by default. X-Forwarded-For is ignored. Only when HTTP_TRUST_PROXY=loopback or explicit with HTTP_TRUST_PROXY_IPS is set to trust exactly the immediate proxy is the validated first IP used.

  • JSON POSTs require application/json with defaults of 64KiB, depth 12, and 5,000 nodes. Fixed route/method/tool allowlists and CSP/frame/referrer/permissions security headers are used; no cross-origin wildcard CORS is provided.

  • All responses return a safe X-Request-ID. Only strictly formatted inbound IDs are accepted and linked to the PostgreSQL audit correlation_id.

  • /api/admin/storage is available only in token mode. The public /ready does not expose DB size, version, or PostgreSQL settings.

  • Node request/operation timeouts and PostgreSQL connection/statement/query timeouts are tunable via .env.example variables. Default DB statement/query timeouts are 8s/10s.

Runtime Repository

DATA_REPOSITORY

Behavior

auto

Requires DATABASE_URL and a ready PostgreSQL; aborts startup on failure

postgres

PostgreSQL required. Startup fails on connection/schema failure; no fallback

memory, disabled

Explicitly uses an empty in-memory repository

All SQL in PostgreSQL mode uses purpose-built parameterized/bounded queries. The pool is closed on shutdown; tool executions record only a salted SHA-256 of the request text plus field types/lengths to mcp_tool_audit on a best-effort basis. Audit write failures remain operational warnings and do not turn successful read results into failures. Set AUDIT_HASH_SALT from a secret manager if cross-deployment hash correlation is needed.

Source Data ETL

Source files are treated as read-only and are never copied or modified.

# 파일을 쓰지 않고 전체 인벤토리·파싱 결과 확인
pnpm etl:dry-run

# data/generated/에 결정적 JSON 산출물 기록(같은 입력이면 unchanged)
pnpm etl

# 공개 포항시의회 의안 갱신(약관 검토 필수; robots 확인 불가 시 별도 승인)
pnpm pohang:crawl -- --acknowledge-terms --acknowledge-robots-unavailable

# 경상북도 보도자료 수집(서술적 맥락용; 통계 근거가 아님)
pnpm gb:press:crawl --accept-terms --since=2020-01-01

# 경상북도의회 의원 명부와 발의의안 수집(약 25분)
pnpm gb:council:crawl --accept-terms

# ETL 재생성 + 스키마 적용 + 구조화 데이터와 PDF 전체 적재
pnpm db:normalize

# 이미 생성한 산출물을 적재(제한된 DB 사용자는 스키마 적용 생략 가능)
pnpm db:load -- --skip-schema

# PDF만 페이지 추출/chunk/로컬 임베딩 후 적재
pnpm pdf:normalize

# 개발·테스트용 파일 제한
pnpm pdf:normalize -- --limit 5

# golden page와 corpus 품질 분포 평가; report는 data/generated/에 기록
pnpm pdf:evaluate
pnpm pdf:evaluate -- --limit 5

# PDF 단계를 생략하거나 db:normalize 안에서 제한
pnpm db:normalize -- --skip-pdf
pnpm db:normalize -- --pdf-limit 5

New Data Intake

Newly received source files should not go directly into datasets/; place them in data/intake/inbox/. They go through six stages: intake → validation → duplicate determination → storage → processing → ledger recording.

# 실제 이동 없이 처리 계획만 확인
pnpm intake:dry-run

# 접수 실행(저장 후 정규화·적재까지)
pnpm intake

# 원장 요약(접수/중복/격리 건수)
pnpm intake:status
  • Validation inspects the actual byte signature, not the extension. HTML error pages saved as .csv or .pdf files that are not PDFs are quarantined with a reason sidecar.

  • Duplicate determination is checked against SHA-256 across the entire source repository, so re-receiving the same file does not double-load it.

  • Storage location is determined by the identified content type, not the filename.

  • Every determination is recorded in the raw_intake_file ledger for post-hoc traceability.

The ETL performs the following:

  1. Catalogs all files' paths (NFC-normalized), sizes, SHA-256, reference date/collection time, and formats

  2. Interprets CSV as UTF-8 first, falling back to CP949

  3. Preserves the 년월 dates in Pohang's yearly population data as-is and identifies various council bill CSVs

  4. Finds 2026년 7월 in the population XLSX title and normalizes Gyeongbuk/city-county values as of 2026-07-31

  5. Recursively scans datasets/new/ and identifies the 9 local tax grains, the official yearbook, and separately quarantined targets by CSV headers or HWPX internal chapter/table markers rather than filenames

  6. Converts explicit won/thousand-won units to KRW integers; Gimcheon's unmarked payment amounts are judged as thousand-won only when they exactly match 6 tax items against the explicitly marked thousand-won collection data

  7. Records raw/cleaned page hashes, extraction method, and quality metrics for PDF sources, promoting only readable official documents to official_observed

  8. Removes only high-confidence repeated headers/footers across documents and chunks speaker transitions and table rows atomically

  9. Beyond the Unicode Korean ratio, checks administrative terms, valid bigrams, particles, rare syllables, and whitespace/punctuation to distinguish extracted_ok, mojibake_suspected, unusable_for_retrieval, and empty

  10. Excluded pages preserve only hash, classification, and rationale; they are not put into chunk/embedding/search

  11. Exposes parsing errors and local tax quality issues via errors.json, local-tax.quality.report.json, and exit codes

data/generated/ is regenerable and therefore excluded from Git.

PostgreSQL Loading

The loader guarantees:

  • Canonical region keys for Gyeongsangbuk-do and the current 22 cities/counties (excluding Gunwi County)

  • Path+SHA-256-based source version preservation. The current run loads 977 sources into the catalog, including the HWPX yearbook, additional separate PDFs, press releases, and council bills

  • 500-row parameterized upserts and single-fact transactions

  • Deterministic council record IDs based on source SHA/path/row number for general CSVs and on original record IDs for Pohang public bills, plus source_locator for population and council data

  • Press releases identified by board post ID (gb-press-<B_STEP>) so re-crawls do not grow rows

  • Gyeongsangbuk-do Council bills identified by the site's 32-hex bill_uid. Bill numbers are reused across terms, so they are not used as keys; sponsors are loaded separately by role (lead/co)

  • Upserts without duplicates on re-run; existing rows absent from a new run are not deleted

  • Fact transaction rollback on failure and ingestion_run.status = failed recording where possible

  • Unresolved regions/invalid rows are recorded in reject counts and error reports without fabricating arbitrary IDs

  • PDF pages/chunks get deterministic IDs from source version, extractor/chunker version, page, and sequence; previous rows are not deleted, but chunks absent from the current extraction are excluded via is_current = false

  • Search uses only approved_current chunks from the single approved version in the lifecycle

  • Embeddings are loaded into PostgreSQL per generation batch, so the full vector corpus is never accumulated in memory

  • Local tax totals are defensively selected in the order of same-scope municipal collection receipts, the Ministry of the Interior and Safety yearbook's city/county tax subtotals, the total local tax in per-capita burden sources, and payment amounts, preserving amount_semantics and source_grain

  • Combined provincial+city/county tax totals are preserved with headlineEligible=false and excluded from tool city/county tax headlines

  • The operational load plan creates only official_observed and catalog_only; it never generates demo_sample

Current expected row counts for provided sources and samples (document_chunk/embedding based on the is_current join):

Table

official_observed

demo_sample

catalog_only

data_source

972

0

5

population_observation

272

0

-

local_tax_revenue

544

0

-

local_tax_collection_status

220

0

-

local_tax_payment_media

1,131

0

-

local_tax_assessment_status

57

0

-

local_tax_per_capita_burden

23

0

-

local_tax_taxpayer_status

378

0

-

local_tax_exemption_status

96

0

-

local_tax_unrefunded_status

64

0

-

local_tax_enforcement_statistic

90

0

-

policy_program

0

0

-

council_record

49,708

0

-

council_member

64

0

-

council_bill

1,454

0

-

press_release

13,707

0

-

document

405

0

3

The 2021·2023·2025·2026 provincial administration work plans in datasets/extra/ are searchable as official page passages. 2020·2022 have CID mojibake, and 2024 has no usable text layer, so only catalog/provenance and exclusion rationale are preserved; they are not included in search.

The overlap between official population and official headline local tax is 16 region-years/5 regions (Bonghwa 20172024, Gumi 20232025, Cheongdo 20202022, Gyeongsan 2025, Pohang 2024). Gumi 20232025 and Cheongdo 20202022 form the official time series using the population stated in the tax-year reports. Since these figures grow when new data is loaded, the test does not fix values but only verifies that the overlap actually exists in both tables. The previous representative period 20222024 is not maintained due to insufficient endpoints; the representative question was changed to the Cheongdo case of 2020~2022, where official comparison is possible.

PDF current item

Count

document_page

12,522

document_page extracted_ok

12,178

Excluded pages (mojibake_suspected/unusable_for_retrieval/empty)

344

document_chunk

24,565

current document_chunk_embedding

24,565

Gyeongsangbuk-do press releases are descriptive context, not statistical facts, so they are stored in a separate press_release table outside the fact model.

Press release item

Value

Records

13,707

Period

2020-01-06 ~ 2026-08-21 (80 months, no gaps)

extracted_ok (searchable)

13,702

unusable_for_retrieval

5 (4 empty bodies, 1 markup unrecoverable)

With attachments

13,674

Gyeongsangbuk-do Council members and bills are also records of council activities, not statistical facts, so they are stored in separate council_member / council_bill / council_bill_sponsor tables.

Council item

Value

Members

64 (People Power Party 58 / Independent 3 / Democratic Party of Korea 3)

Electoral district → region_id mapping

56 (8 proportional representation members are null)

Bills

1,454 (2006-12-26 ~ 2026-08-14)

Sponsor relations

24,178 (lead sponsorship 1,449 / co-sponsorship 22,729)

Sponsors linked to the current roster

8,529 (the rest are members of past terms, names only preserved)

[!IMPORTANT] The existing council_record is the Pohang City Council minutes, and the three council_* tables are the Gyeongsangbuk-do Council. They are different bodies, so they must not be joined or summed. Summing lead motions (lead) and co-sponsorships (co) also overestimates council activity.

SELECT data_status, count(*) FROM data_source GROUP BY data_status;
SELECT data_status, count(*) FROM population_observation GROUP BY data_status;
SELECT data_status, count(*) FROM local_tax_revenue GROUP BY data_status;
SELECT severity, issue_code, count(*) FROM data_quality_issue
GROUP BY severity, issue_code ORDER BY severity, issue_code;
SELECT data_status, count(*) FROM council_record GROUP BY data_status;
SELECT content_classification, count(*) FROM press_release
GROUP BY content_classification;
SELECT entity_type, count(*) FROM source_locator GROUP BY entity_type;
SELECT text_class, count(*) FROM document_page WHERE is_current GROUP BY text_class;
SELECT content_classification, count(*) FROM document_page
WHERE is_current GROUP BY content_classification;
SELECT is_current, count(*) FROM document_chunk GROUP BY is_current;

PDF source lifecycle and storage operations

New extractors/chunkers are registered as candidate without automatically changing production search. Approval/rollback switches visibility in a single transaction after verifying that retained chunks and embeddings are complete. Rolling back to a past version can temporarily hide new documents that did not exist at the time, so the returned document/page/chunk coverage must be checked.

# version, current/stale 행, relation/index 크기, 안전한 DB 설정과 경고
pnpm lifecycle:status

# candidate 승인 또는 retained retired version rollback
pnpm lifecycle:approve -- \
  --version 'extractor::chunker' \
  --actor operator-name \
  --reason 'Golden/corpus evaluation passed'

# active approved version은 직접 retire할 수 없음
pnpm lifecycle:retire -- \
  --version 'extractor::old-chunker' \
  --actor operator-name \
  --reason 'Superseded and retained for rollback'

# 기본은 30일 보존 dry-run. 출력의 exact token 없이는 삭제 불가
pnpm lifecycle:purge -- --actor operator-name --retention-days 30
pnpm lifecycle:purge -- \
  --execute --confirm PLAN_TOKEN --batch-size 500 \
  --max-rows 10000 --max-runtime-seconds 60

# 만료 audit도 먼저 dry-run하고, 삭제 전 일별 aggregate를 보존
pnpm lifecycle:audit-purge -- --actor operator-name

# 기본은 allowlist 테이블 ANALYZE-only, 최대 10분
pnpm lifecycle:maintain -- --actor operator-name

Purge uses an advisory lock, minimum retention period, approved/current protection, FK-safe batches, and an exact plan hash. If the candidate changes, the existing token is not executed. VACUUM is not run automatically and requires both --vacuum --confirm-vacuum. The previous measured 53100 was due to insufficient shared memory, so do not blindly retry; check /dev/shm, shm_size, disk, maintenance_work_mem, and parallel maintenance settings.

After approving a new policy extraction version, the previous 23,795 and legacy 17,487, totaling 41,282 stale chunks/embeddings are preserved. The actual stale corpus is not deleted until operator approval. Purge does not delete base document/data_source provenance; it only cleans up per-version processing status. Reclaiming physical space after logical deletion is the responsibility of VACUUM in a separate maintenance window.

The default provider is the network-free 384-dimensional local/hashed-korean-token/1-d384. It is a retrieval-only lexical vector created by feature-hashing Korean words and 2/3-character tokens, and does not claim semantic embedding quality. Embeddings with the same content hash are not regenerated on re-runs.

# 기본: 로컬, 무료, 결정적
pnpm pdf:normalize -- --provider hashed

# 명시적으로 외부 전송을 허용한 경우에만 OpenAI 사용
OPENAI_API_KEY=... pnpm pdf:normalize -- \
  --provider openai --allow-external

Since dimensions can differ per provider, embeddings are stored as document_chunk_embedding.embedding real[] with a dimension check, not as JSONB. The current environment does not require pgvector ANN; it builds up to 200 lexical candidates using the pg_trgm and pgcrypto extensions installed by the schema and PostgreSQL FTS, then cosine-reranks. The dedicated PostgresHybridSearchRepository is connected to the council search of the PostgreSQL MCP repository and returns structured bills and PDF chunks together.

The full local run result is 408 PDFs, 12,522 pages, and 24,565 current chunks/embeddings. Pages are extracted_ok 12,178, mojibake_suspected 172, unusable_for_retrieval 166, and empty 6, with 0 chunks generated from excluded pages. Re-scanning only the existing 401-document corpus yields all 11,712 pages as extracted_ok. 543 lines of repeated headers were removed.

pnpm pdf:evaluate evaluates 6 local golden pages that store only short markers and the full corpus distribution, producing data/generated/pdf.quality.report.json. Golden failures, failed pages exceeding 1%, excluded-page chunk contamination, and policy documents that are readable but fail to verify year/document-type evidence are treated as severe regressions.

Quality commands

pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm pdf:evaluate

Repository structure

prototype/                Next.js 화면(8787이 여기로 넘긴다)
services/mcp-server/      MCP 서버(stdio · Streamable HTTP) + HTTP API
packages/domain/          Zod 입력, 공통 타입, 분석·초안 로직
packages/retrieval/       한글 lexical/hybrid 검색
packages/evaluation/      평가 결과 판정
pipelines/                원천 파일 인벤토리와 정규화
db/                       PostgreSQL 스키마와 마이그레이션
services/mcp-server/test-fixtures/demo/  demo 거부 동작만 검증하는 테스트 fixture
evals/                    대표 평가 질문
docs/                     설계·도구·데이터 사전·변경 기록

For details, see Architecture, Data Dictionary, and MCP Tool Specification.

License

The code is under the MIT License. The license of the source public data follows the terms of each provider, and this repository does not include the source datasets/.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Enables users to query Korean demographic statistics (population, households, housing) and view them as choropleth maps via MCP tools.
    -
  • A
    license
    A
    quality
    B
    maintenance
    Enables natural language querying of Korean statistical data from KOSIS, including population, employment, GDP, housing prices, and more, with support for regional and trend analysis.
    8
    8 npm
    16
    MIT
  • F
    license
    A
    quality
    C
    maintenance
    Enables querying Korean public datasets (apartment prices, weather, air quality) via natural language using an MCP server and local LLM agent.
    5
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Korean public-data MCP servers for AI agents, enabling natural language queries to KOSIS statistics and other Korean official data sources without requiring local accounts or API keys.
    -