get_urban_facility
Identify overlaps with urban planning facilities (roads, parks, utilities) for a Korean land parcel, revealing potential building restrictions from law violations or unexecuted plans.
Instructions
Return 도시계획시설 overlaps: 도로, 교통시설, 공간시설(공원·녹지), 유통공급, 공공문화체육, 방재, 보건위생, 환경기초, 기타기반시설. Overlap with 도시계획시설 triggers 건축제한 (국계법 제64조) or 미집행 저촉 리스크. Exact 저촉 면적 needs geometric intersection, not returned here.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Korean address (e.g. '경기도 평택시 포승읍 내기리 680') or 19-digit PNU | |
| radius_m | No | 접함(nearby) 후보 탐지 반경(미터). 기본 50. 0이면 저촉(overlap)만 반환. 최대 500. |
Implementation Reference
- src/tools/get_urban_facility.ts:17-77 (handler)The main handler function. Resolves the query to a point, then queries 9 V-World urban facility layers (도시계획시설) for both overlap (저촉) and nearby (접함) results. Returns structured JSON with facilities, counts, errors, and a disclaimer.
export const getUrbanFacilityTool = async ({ query, radius_m, }: { query: string; radius_m?: number; }) => { const resolved = await resolveToPoint(query); const effectiveRadius = typeof radius_m === "number" ? radius_m : 50; const [overlapQ, proximityQ] = await Promise.all([ queryOverlays(LAYERS, resolved.point_wgs84, { perLayerSize: 10, radius_m: 0 }), effectiveRadius > 0 ? queryOverlays(LAYERS, resolved.point_wgs84, { perLayerSize: 10, radius_m: effectiveRadius }) : Promise.resolve({ hits: [], errors: [] }), ]); const overlapIds = new Set(overlapQ.hits.map((h) => `${h.layer}:${h.name}:${h.properties?.present_sn ?? ""}`)); const nearbyOnly = proximityQ.hits.filter( (h) => !overlapIds.has(`${h.layer}:${h.name}:${h.properties?.present_sn ?? ""}`) ); const shape = (h: typeof overlapQ.hits[number], relation: "overlap" | "nearby") => ({ relation, layer: h.layer, category: h.layer_label, facility_name: h.name, grade: h.properties?.grad_se ?? undefined, facility_number: h.properties?.road_no ?? undefined, execution_status: h.properties?.exc_nam ?? undefined, designation_year: h.designation_year, designation_number: h.designation_number, sido: h.sido, sigg: h.sigg, }); const overlays = [ ...overlapQ.hits.map((h) => shape(h, "overlap")), ...nearbyOnly.map((h) => shape(h, "nearby")), ]; const result = { query, pnu: resolved.pnu, point_wgs84: resolved.point_wgs84, radius_m: effectiveRadius, overlap_count: overlapQ.hits.length, nearby_count: nearbyOnly.length, facilities: overlays, layer_errors: [...overlapQ.errors, ...proximityQ.errors], source: { resolved_at: new Date().toISOString(), layers: LAYERS.map((l) => l.id), }, note: "relation='overlap' = 필지와 저촉 (점 기반 판정), 'nearby' = 반경 radius_m 이내 접함 후보. 정확한 저촉·접함 구분은 필지 폴리곤 교차로 재검증 필요 (국계법 제64조 저촉 vs 접함 구분은 인허가 영향 상이). radius_m=0을 넘기면 접함 후보는 비활성화.", disclaimer: DISCLAIMER, }; return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; }; - src/tools/get_urban_facility.ts:5-15 (schema)Layer definitions for the 9 urban facility categories: 도로, 교통시설, 공간시설, 유통공급시설, 공공문화체육시설, 방재시설, 보건위생시설, 환경기초시설, 기타기반시설. Map V-World layer IDs (LT_C_UPISUQ151-159) to Korean labels.
const LAYERS: LayerDef[] = [ { id: "LT_C_UPISUQ151", label: "도로" }, { id: "LT_C_UPISUQ152", label: "교통시설" }, { id: "LT_C_UPISUQ153", label: "공간시설" }, { id: "LT_C_UPISUQ154", label: "유통공급시설" }, { id: "LT_C_UPISUQ155", label: "공공문화체육시설" }, { id: "LT_C_UPISUQ156", label: "방재시설" }, { id: "LT_C_UPISUQ157", label: "보건위생시설" }, { id: "LT_C_UPISUQ158", label: "환경기초시설" }, { id: "LT_C_UPISUQ159", label: "기타기반시설" }, ]; - src/server.ts:54-70 (registration)Registration of the tool with the MCP server. The tool is named 'get_urban_facility', accepts a 'query' (string, min 2 chars) and optional 'radius_m' (int 0-500), and is wired to the getUrbanFacilityTool handler.
server.tool( "get_urban_facility", "Return 도시계획시설 overlaps: 도로, 교통시설, 공간시설(공원·녹지), 유통공급, 공공문화체육, 방재, 보건위생, 환경기초, 기타기반시설. Overlap with 도시계획시설 triggers 건축제한 (국계법 제64조) or 미집행 저촉 리스크. Exact 저촉 면적 needs geometric intersection, not returned here.", { ...QUERY_SCHEMA, radius_m: z .number() .int() .min(0) .max(500) .optional() .describe( "접함(nearby) 후보 탐지 반경(미터). 기본 50. 0이면 저촉(overlap)만 반환. 최대 500." ), }, getUrbanFacilityTool ); - src/lib/overlays.ts:83-128 (helper)The queryOverlays helper function that performs the actual spatial queries against V-World for each layer, returning hits and errors. Used by get_urban_facility to query both overlap (point-based) and proximity (radius-based) results.
export async function queryOverlays( layers: LayerDef[], point: { x: number; y: number }, opts: QueryOverlaysOptions | number = {} ): Promise<OverlayQueryResult> { const options: QueryOverlaysOptions = typeof opts === "number" ? { perLayerSize: opts } : opts; const perLayerSize = options.perLayerSize ?? 5; const geomFilter = options.geomFilter ?? (options.radius_m && options.radius_m > 0 ? pointToBox(point, options.radius_m) : undefined); const results = await Promise.all( layers.map(async (layer) => { try { const feats = geomFilter ? await getFeatures({ layer: layer.id, geomFilter, size: perLayerSize }) : await getFeatures({ layer: layer.id, point, size: perLayerSize }); return { layer, feats, error: null as LayerQueryError | null }; } catch (e) { const err: LayerQueryError = e instanceof VWorldError ? { layer: layer.id, layer_label: layer.label, error_code: e.code ?? "UNKNOWN", error_message: e.message, } : { layer: layer.id, layer_label: layer.label, error_code: "UNHANDLED", error_message: e instanceof Error ? e.message : String(e), }; return { layer, feats: [] as VWorldFeature[], error: err }; } }) ); const hits: OverlayHit[] = []; const errors: LayerQueryError[] = []; for (const r of results) { if (r.error) errors.push(r.error); for (const f of r.feats) hits.push(featureToHit(r.layer, f)); } return { hits, errors }; } - src/lib/resolve.ts:151-163 (helper)The resolveToPoint helper that converts an address or PNU string into a spatial point (WGS84 coordinates) used as the basis for the urban facility queries.
export async function resolveToPoint(query: string): Promise<ResolvedPoint> { const r = await resolveParcelCore(query, false); const { _parcel_geometry, ...rest } = r; void _parcel_geometry; return rest; } export async function resolveToParcel(query: string): Promise<ResolvedParcel> { const r = await resolveParcelCore(query, true); const { _parcel_geometry, ...rest } = r; const wkt = geometryToWKT(_parcel_geometry); return { ...rest, parcel_geometry: _parcel_geometry ?? null, parcel_wkt: wkt }; }