gyeongbuk-public-ai
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@gyeongbuk-public-ai경주시 최근 5년 인구 변화와 지방세 수입 추이를 알려줘"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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_sampleoperational 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) |
| 50.0% → 81.8% (+31.8p) | 50.0% → 16.7% | 0.0% → 80.0% |
| 27.3% → 72.7% (+45.5p) | 33.3% → 22.2% | 0.0% → 56.8% |
| 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
abstentioncategory (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_knowledgecategory (control) is deliberately designed so vanilla can win.
Quick Start
Requirements: Node.js 22+, pnpm 10+
pnpm install
pnpm test
pnpm build
pnpm startOpen 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 |
| UI (3100) + MCP server (8787) |
| Same setup in development mode |
| MCP server only |
| 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 devDocker
HTTP_AUTH_TOKEN="$(openssl rand -hex 32)" docker compose up --buildWeb/API: http://localhost:8787
PostgreSQL:
localhost:5432Initial schema:
db/schema.sql
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:schemaDATABASE_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:stdioThe 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 --httpRemote MCP client registration:
{
"mcpServers": {
"g-bap": {
"type": "http",
"url": "https://<host>/mcp",
"headers": { "Authorization": "Bearer <HTTP_AUTH_TOKEN>" }
}
}
}Variable | Default | Description |
|
| Concurrent session cap. Returns |
|
| Idle session reclamation threshold |
| (empty) | Host allow-list for DNS rebinding defense |
| (empty) | Same-origin allow-list |
On public hosting,
HTTP_AUTH_MODE=tokenis mandatory. Failed authentication does not create a session and disconnects with401.If both
MCP_ALLOWED_HOSTSandMCP_ALLOWED_ORIGINSare empty, DNS rebinding defense is off. Set them unless you are behind a trusted proxy.Protocol traffic on
/mcpconsumes the cheap budget; expensive tools withintools/calladditionally consume the expensive budget. See docs/CHANGES_HOSTED_MCP_SERVER.md for details.
HTTP API
Method | Path | Description |
|
| Process liveness check |
|
| Exposes only minimal ready/not-ready status |
|
| Representative question list |
|
| Token-only DB capacity, lifecycle, and safe settings summary |
|
| Accepts a natural-language question and performs dataset exploration, tool selection, and evidence collection |
|
| 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=tokenrequiresAuthorization: BearerorX-API-Keyon/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
429withRetry-After.The client is the socket IP by default.
X-Forwarded-Foris ignored. Only whenHTTP_TRUST_PROXY=loopbackorexplicitwithHTTP_TRUST_PROXY_IPSis set to trust exactly the immediate proxy is the validated first IP used.JSON POSTs require
application/jsonwith 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 auditcorrelation_id./api/admin/storageis available only in token mode. The public/readydoes not expose DB size, version, or PostgreSQL settings.Node request/operation timeouts and PostgreSQL connection/statement/query timeouts are tunable via
.env.examplevariables. Default DB statement/query timeouts are 8s/10s.
Runtime Repository
| Behavior |
| Requires |
| PostgreSQL required. Startup fails on connection/schema failure; no fallback |
| 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 5New 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:statusValidation inspects the actual byte signature, not the extension. HTML error pages saved as
.csvor.pdffiles 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_fileledger for post-hoc traceability.
The ETL performs the following:
Catalogs all files' paths (NFC-normalized), sizes, SHA-256, reference date/collection time, and formats
Interprets CSV as UTF-8 first, falling back to CP949
Preserves the
년월dates in Pohang's yearly population data as-is and identifies various council bill CSVsFinds
2026년 7월in the population XLSX title and normalizes Gyeongbuk/city-county values as of2026-07-31Recursively 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 filenamesConverts 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
Records raw/cleaned page hashes, extraction method, and quality metrics for PDF sources, promoting only readable official documents to
official_observedRemoves only high-confidence repeated headers/footers across documents and chunks speaker transitions and table rows atomically
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, andemptyExcluded pages preserve only hash, classification, and rationale; they are not put into chunk/embedding/search
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_locatorfor population and council dataPress releases identified by board post ID (
gb-press-<B_STEP>) so re-crawls do not grow rowsGyeongsangbuk-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 byrole(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 = failedrecording where possibleUnresolved 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 = falseSearch uses only
approved_currentchunks from the singleapprovedversion in the lifecycleEmbeddings 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_semanticsandsource_grainCombined provincial+city/county tax totals are preserved with
headlineEligible=falseand excluded from tool city/county tax headlinesThe operational load plan creates only
official_observedandcatalog_only; it never generatesdemo_sample
Current expected row counts for provided sources and samples (document_chunk/embedding based on the is_current join):
Table |
|
|
|
| 972 | 0 | 5 |
| 272 | 0 | - |
| 544 | 0 | - |
| 220 | 0 | - |
| 1,131 | 0 | - |
| 57 | 0 | - |
| 23 | 0 | - |
| 378 | 0 | - |
| 96 | 0 | - |
| 64 | 0 | - |
| 90 | 0 | - |
| 0 | 0 | - |
| 49,708 | 0 | - |
| 64 | 0 | - |
| 1,454 | 0 | - |
| 13,707 | 0 | - |
| 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 |
| 12,522 |
| 12,178 |
Excluded pages ( | 344 |
| 24,565 |
current | 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) |
| 13,702 |
| 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 → | 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_recordis the Pohang City Council minutes, and the threecouncil_*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-namePurge 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.
PDF embedding and search
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-externalSince 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:evaluateRepository 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/.
This server cannot be deployed
Maintenance
Related MCP Connectors
- mcpweaveOAuthcom.mcpweave
Korea-native MCP gateway: Korean commerce, payments, messaging, gov & finance APIs for AI agents.
Korean national tax and social insurance filings, invoices and payroll data as MCP tools.
Korean fact-verification tools for AI agents: business registration, address, DART, apt prices, laws
Who qualifies for 10,956 Korean government benefits (보조금24). Plus public data and weather.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables users to query Korean demographic statistics (population, households, housing) and view them as choropleth maps via MCP tools.-
- AlicenseAqualityBmaintenanceEnables natural language querying of Korean statistical data from KOSIS, including population, employment, GDP, housing prices, and more, with support for regional and trend analysis.88 npm16MIT
- FlicenseAqualityCmaintenanceEnables querying Korean public datasets (apartment prices, weather, air quality) via natural language using an MCP server and local LLM agent.5-
- FlicenseNot gradedqualityDmaintenanceKorean 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.-