├── .env.example ├── .gitignore ├── .pnp.cjs ├── .pnp.loader.mjs ├── .yarn └── install-state.gz ├── LICENSE ├── README.md ├── mcp.json.example ├── package.json ├── src └── index.ts ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | # Kakao REST API Key 2 | # 카카오 개발자 사이트에서 REST API 키를 얻으세요: https://developers.kakao.com/ 3 | KAKAO_REST_API_KEY=your_kakao_rest_api_key_here 4 | 5 | # 서버 설정 (선택사항) 6 | # MCP_SERVER_PORT=3000 7 | # MCP_SERVER_MODE=http # http 또는 stdio -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | .pnp/ 4 | .pnp.js 5 | 6 | # testing 7 | coverage/ 8 | 9 | # production 10 | dist/ 11 | build/ 12 | 13 | # environment 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | mcp.json 20 | 21 | # logs 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # editor 27 | .idea/ 28 | .vscode/ 29 | *.swp 30 | *.swo 31 | 32 | # OS 33 | .DS_Store 34 | Thumbs.db -------------------------------------------------------------------------------- /.pnp.loader.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | 4 | import fs from 'fs'; 5 | import { URL as URL$1, fileURLToPath, pathToFileURL } from 'url'; 6 | import path from 'path'; 7 | import { createHash } from 'crypto'; 8 | import { EOL } from 'os'; 9 | import esmModule, { createRequire, isBuiltin } from 'module'; 10 | import assert from 'assert'; 11 | 12 | const SAFE_TIME = 456789e3; 13 | 14 | const PortablePath = { 15 | root: `/`, 16 | dot: `.`, 17 | parent: `..` 18 | }; 19 | const npath = Object.create(path); 20 | const ppath = Object.create(path.posix); 21 | npath.cwd = () => process.cwd(); 22 | ppath.cwd = process.platform === `win32` ? () => toPortablePath(process.cwd()) : process.cwd; 23 | if (process.platform === `win32`) { 24 | ppath.resolve = (...segments) => { 25 | if (segments.length > 0 && ppath.isAbsolute(segments[0])) { 26 | return path.posix.resolve(...segments); 27 | } else { 28 | return path.posix.resolve(ppath.cwd(), ...segments); 29 | } 30 | }; 31 | } 32 | const contains = function(pathUtils, from, to) { 33 | from = pathUtils.normalize(from); 34 | to = pathUtils.normalize(to); 35 | if (from === to) 36 | return `.`; 37 | if (!from.endsWith(pathUtils.sep)) 38 | from = from + pathUtils.sep; 39 | if (to.startsWith(from)) { 40 | return to.slice(from.length); 41 | } else { 42 | return null; 43 | } 44 | }; 45 | npath.contains = (from, to) => contains(npath, from, to); 46 | ppath.contains = (from, to) => contains(ppath, from, to); 47 | const WINDOWS_PATH_REGEXP = /^([a-zA-Z]:.*)$/; 48 | const UNC_WINDOWS_PATH_REGEXP = /^\/\/(\.\/)?(.*)$/; 49 | const PORTABLE_PATH_REGEXP = /^\/([a-zA-Z]:.*)$/; 50 | const UNC_PORTABLE_PATH_REGEXP = /^\/unc\/(\.dot\/)?(.*)$/; 51 | function fromPortablePathWin32(p) { 52 | let portablePathMatch, uncPortablePathMatch; 53 | if (portablePathMatch = p.match(PORTABLE_PATH_REGEXP)) 54 | p = portablePathMatch[1]; 55 | else if (uncPortablePathMatch = p.match(UNC_PORTABLE_PATH_REGEXP)) 56 | p = `\\\\${uncPortablePathMatch[1] ? `.\\` : ``}${uncPortablePathMatch[2]}`; 57 | else 58 | return p; 59 | return p.replace(/\//g, `\\`); 60 | } 61 | function toPortablePathWin32(p) { 62 | p = p.replace(/\\/g, `/`); 63 | let windowsPathMatch, uncWindowsPathMatch; 64 | if (windowsPathMatch = p.match(WINDOWS_PATH_REGEXP)) 65 | p = `/${windowsPathMatch[1]}`; 66 | else if (uncWindowsPathMatch = p.match(UNC_WINDOWS_PATH_REGEXP)) 67 | p = `/unc/${uncWindowsPathMatch[1] ? `.dot/` : ``}${uncWindowsPathMatch[2]}`; 68 | return p; 69 | } 70 | const toPortablePath = process.platform === `win32` ? toPortablePathWin32 : (p) => p; 71 | const fromPortablePath = process.platform === `win32` ? fromPortablePathWin32 : (p) => p; 72 | npath.fromPortablePath = fromPortablePath; 73 | npath.toPortablePath = toPortablePath; 74 | function convertPath(targetPathUtils, sourcePath) { 75 | return targetPathUtils === npath ? fromPortablePath(sourcePath) : toPortablePath(sourcePath); 76 | } 77 | 78 | const defaultTime = new Date(SAFE_TIME * 1e3); 79 | const defaultTimeMs = defaultTime.getTime(); 80 | async function copyPromise(destinationFs, destination, sourceFs, source, opts) { 81 | const normalizedDestination = destinationFs.pathUtils.normalize(destination); 82 | const normalizedSource = sourceFs.pathUtils.normalize(source); 83 | const prelayout = []; 84 | const postlayout = []; 85 | const { atime, mtime } = opts.stableTime ? { atime: defaultTime, mtime: defaultTime } : await sourceFs.lstatPromise(normalizedSource); 86 | await destinationFs.mkdirpPromise(destinationFs.pathUtils.dirname(destination), { utimes: [atime, mtime] }); 87 | await copyImpl(prelayout, postlayout, destinationFs, normalizedDestination, sourceFs, normalizedSource, { ...opts, didParentExist: true }); 88 | for (const operation of prelayout) 89 | await operation(); 90 | await Promise.all(postlayout.map((operation) => { 91 | return operation(); 92 | })); 93 | } 94 | async function copyImpl(prelayout, postlayout, destinationFs, destination, sourceFs, source, opts) { 95 | const destinationStat = opts.didParentExist ? await maybeLStat(destinationFs, destination) : null; 96 | const sourceStat = await sourceFs.lstatPromise(source); 97 | const { atime, mtime } = opts.stableTime ? { atime: defaultTime, mtime: defaultTime } : sourceStat; 98 | let updated; 99 | switch (true) { 100 | case sourceStat.isDirectory(): 101 | { 102 | updated = await copyFolder(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts); 103 | } 104 | break; 105 | case sourceStat.isFile(): 106 | { 107 | updated = await copyFile(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts); 108 | } 109 | break; 110 | case sourceStat.isSymbolicLink(): 111 | { 112 | updated = await copySymlink(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts); 113 | } 114 | break; 115 | default: { 116 | throw new Error(`Unsupported file type (${sourceStat.mode})`); 117 | } 118 | } 119 | if (opts.linkStrategy?.type !== `HardlinkFromIndex` || !sourceStat.isFile()) { 120 | if (updated || destinationStat?.mtime?.getTime() !== mtime.getTime() || destinationStat?.atime?.getTime() !== atime.getTime()) { 121 | postlayout.push(() => destinationFs.lutimesPromise(destination, atime, mtime)); 122 | updated = true; 123 | } 124 | if (destinationStat === null || (destinationStat.mode & 511) !== (sourceStat.mode & 511)) { 125 | postlayout.push(() => destinationFs.chmodPromise(destination, sourceStat.mode & 511)); 126 | updated = true; 127 | } 128 | } 129 | return updated; 130 | } 131 | async function maybeLStat(baseFs, p) { 132 | try { 133 | return await baseFs.lstatPromise(p); 134 | } catch (e) { 135 | return null; 136 | } 137 | } 138 | async function copyFolder(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts) { 139 | if (destinationStat !== null && !destinationStat.isDirectory()) { 140 | if (opts.overwrite) { 141 | prelayout.push(async () => destinationFs.removePromise(destination)); 142 | destinationStat = null; 143 | } else { 144 | return false; 145 | } 146 | } 147 | let updated = false; 148 | if (destinationStat === null) { 149 | prelayout.push(async () => { 150 | try { 151 | await destinationFs.mkdirPromise(destination, { mode: sourceStat.mode }); 152 | } catch (err) { 153 | if (err.code !== `EEXIST`) { 154 | throw err; 155 | } 156 | } 157 | }); 158 | updated = true; 159 | } 160 | const entries = await sourceFs.readdirPromise(source); 161 | const nextOpts = opts.didParentExist && !destinationStat ? { ...opts, didParentExist: false } : opts; 162 | if (opts.stableSort) { 163 | for (const entry of entries.sort()) { 164 | if (await copyImpl(prelayout, postlayout, destinationFs, destinationFs.pathUtils.join(destination, entry), sourceFs, sourceFs.pathUtils.join(source, entry), nextOpts)) { 165 | updated = true; 166 | } 167 | } 168 | } else { 169 | const entriesUpdateStatus = await Promise.all(entries.map(async (entry) => { 170 | await copyImpl(prelayout, postlayout, destinationFs, destinationFs.pathUtils.join(destination, entry), sourceFs, sourceFs.pathUtils.join(source, entry), nextOpts); 171 | })); 172 | if (entriesUpdateStatus.some((status) => status)) { 173 | updated = true; 174 | } 175 | } 176 | return updated; 177 | } 178 | async function copyFileViaIndex(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts, linkStrategy) { 179 | const sourceHash = await sourceFs.checksumFilePromise(source, { algorithm: `sha1` }); 180 | const defaultMode = 420; 181 | const sourceMode = sourceStat.mode & 511; 182 | const indexFileName = `${sourceHash}${sourceMode !== defaultMode ? sourceMode.toString(8) : ``}`; 183 | const indexPath = destinationFs.pathUtils.join(linkStrategy.indexPath, sourceHash.slice(0, 2), `${indexFileName}.dat`); 184 | let AtomicBehavior; 185 | ((AtomicBehavior2) => { 186 | AtomicBehavior2[AtomicBehavior2["Lock"] = 0] = "Lock"; 187 | AtomicBehavior2[AtomicBehavior2["Rename"] = 1] = "Rename"; 188 | })(AtomicBehavior || (AtomicBehavior = {})); 189 | let atomicBehavior = 1 /* Rename */; 190 | let indexStat = await maybeLStat(destinationFs, indexPath); 191 | if (destinationStat) { 192 | const isDestinationHardlinkedFromIndex = indexStat && destinationStat.dev === indexStat.dev && destinationStat.ino === indexStat.ino; 193 | const isIndexModified = indexStat?.mtimeMs !== defaultTimeMs; 194 | if (isDestinationHardlinkedFromIndex) { 195 | if (isIndexModified && linkStrategy.autoRepair) { 196 | atomicBehavior = 0 /* Lock */; 197 | indexStat = null; 198 | } 199 | } 200 | if (!isDestinationHardlinkedFromIndex) { 201 | if (opts.overwrite) { 202 | prelayout.push(async () => destinationFs.removePromise(destination)); 203 | destinationStat = null; 204 | } else { 205 | return false; 206 | } 207 | } 208 | } 209 | const tempPath = !indexStat && atomicBehavior === 1 /* Rename */ ? `${indexPath}.${Math.floor(Math.random() * 4294967296).toString(16).padStart(8, `0`)}` : null; 210 | let tempPathCleaned = false; 211 | prelayout.push(async () => { 212 | if (!indexStat) { 213 | if (atomicBehavior === 0 /* Lock */) { 214 | await destinationFs.lockPromise(indexPath, async () => { 215 | const content = await sourceFs.readFilePromise(source); 216 | await destinationFs.writeFilePromise(indexPath, content); 217 | }); 218 | } 219 | if (atomicBehavior === 1 /* Rename */ && tempPath) { 220 | const content = await sourceFs.readFilePromise(source); 221 | await destinationFs.writeFilePromise(tempPath, content); 222 | try { 223 | await destinationFs.linkPromise(tempPath, indexPath); 224 | } catch (err) { 225 | if (err.code === `EEXIST`) { 226 | tempPathCleaned = true; 227 | await destinationFs.unlinkPromise(tempPath); 228 | } else { 229 | throw err; 230 | } 231 | } 232 | } 233 | } 234 | if (!destinationStat) { 235 | await destinationFs.linkPromise(indexPath, destination); 236 | } 237 | }); 238 | postlayout.push(async () => { 239 | if (!indexStat) { 240 | await destinationFs.lutimesPromise(indexPath, defaultTime, defaultTime); 241 | if (sourceMode !== defaultMode) { 242 | await destinationFs.chmodPromise(indexPath, sourceMode); 243 | } 244 | } 245 | if (tempPath && !tempPathCleaned) { 246 | await destinationFs.unlinkPromise(tempPath); 247 | } 248 | }); 249 | return false; 250 | } 251 | async function copyFileDirect(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts) { 252 | if (destinationStat !== null) { 253 | if (opts.overwrite) { 254 | prelayout.push(async () => destinationFs.removePromise(destination)); 255 | destinationStat = null; 256 | } else { 257 | return false; 258 | } 259 | } 260 | prelayout.push(async () => { 261 | const content = await sourceFs.readFilePromise(source); 262 | await destinationFs.writeFilePromise(destination, content); 263 | }); 264 | return true; 265 | } 266 | async function copyFile(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts) { 267 | if (opts.linkStrategy?.type === `HardlinkFromIndex`) { 268 | return copyFileViaIndex(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts, opts.linkStrategy); 269 | } else { 270 | return copyFileDirect(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts); 271 | } 272 | } 273 | async function copySymlink(prelayout, postlayout, destinationFs, destination, destinationStat, sourceFs, source, sourceStat, opts) { 274 | if (destinationStat !== null) { 275 | if (opts.overwrite) { 276 | prelayout.push(async () => destinationFs.removePromise(destination)); 277 | destinationStat = null; 278 | } else { 279 | return false; 280 | } 281 | } 282 | prelayout.push(async () => { 283 | await destinationFs.symlinkPromise(convertPath(destinationFs.pathUtils, await sourceFs.readlinkPromise(source)), destination); 284 | }); 285 | return true; 286 | } 287 | 288 | class FakeFS { 289 | pathUtils; 290 | constructor(pathUtils) { 291 | this.pathUtils = pathUtils; 292 | } 293 | async *genTraversePromise(init, { stableSort = false } = {}) { 294 | const stack = [init]; 295 | while (stack.length > 0) { 296 | const p = stack.shift(); 297 | const entry = await this.lstatPromise(p); 298 | if (entry.isDirectory()) { 299 | const entries = await this.readdirPromise(p); 300 | if (stableSort) { 301 | for (const entry2 of entries.sort()) { 302 | stack.push(this.pathUtils.join(p, entry2)); 303 | } 304 | } else { 305 | throw new Error(`Not supported`); 306 | } 307 | } else { 308 | yield p; 309 | } 310 | } 311 | } 312 | async checksumFilePromise(path, { algorithm = `sha512` } = {}) { 313 | const fd = await this.openPromise(path, `r`); 314 | try { 315 | const CHUNK_SIZE = 65536; 316 | const chunk = Buffer.allocUnsafeSlow(CHUNK_SIZE); 317 | const hash = createHash(algorithm); 318 | let bytesRead = 0; 319 | while ((bytesRead = await this.readPromise(fd, chunk, 0, CHUNK_SIZE)) !== 0) 320 | hash.update(bytesRead === CHUNK_SIZE ? chunk : chunk.slice(0, bytesRead)); 321 | return hash.digest(`hex`); 322 | } finally { 323 | await this.closePromise(fd); 324 | } 325 | } 326 | async removePromise(p, { recursive = true, maxRetries = 5 } = {}) { 327 | let stat; 328 | try { 329 | stat = await this.lstatPromise(p); 330 | } catch (error) { 331 | if (error.code === `ENOENT`) { 332 | return; 333 | } else { 334 | throw error; 335 | } 336 | } 337 | if (stat.isDirectory()) { 338 | if (recursive) { 339 | const entries = await this.readdirPromise(p); 340 | await Promise.all(entries.map((entry) => { 341 | return this.removePromise(this.pathUtils.resolve(p, entry)); 342 | })); 343 | } 344 | for (let t = 0; t <= maxRetries; t++) { 345 | try { 346 | await this.rmdirPromise(p); 347 | break; 348 | } catch (error) { 349 | if (error.code !== `EBUSY` && error.code !== `ENOTEMPTY`) { 350 | throw error; 351 | } else if (t < maxRetries) { 352 | await new Promise((resolve) => setTimeout(resolve, t * 100)); 353 | } 354 | } 355 | } 356 | } else { 357 | await this.unlinkPromise(p); 358 | } 359 | } 360 | removeSync(p, { recursive = true } = {}) { 361 | let stat; 362 | try { 363 | stat = this.lstatSync(p); 364 | } catch (error) { 365 | if (error.code === `ENOENT`) { 366 | return; 367 | } else { 368 | throw error; 369 | } 370 | } 371 | if (stat.isDirectory()) { 372 | if (recursive) 373 | for (const entry of this.readdirSync(p)) 374 | this.removeSync(this.pathUtils.resolve(p, entry)); 375 | this.rmdirSync(p); 376 | } else { 377 | this.unlinkSync(p); 378 | } 379 | } 380 | async mkdirpPromise(p, { chmod, utimes } = {}) { 381 | p = this.resolve(p); 382 | if (p === this.pathUtils.dirname(p)) 383 | return void 0; 384 | const parts = p.split(this.pathUtils.sep); 385 | let createdDirectory; 386 | for (let u = 2; u <= parts.length; ++u) { 387 | const subPath = parts.slice(0, u).join(this.pathUtils.sep); 388 | if (!this.existsSync(subPath)) { 389 | try { 390 | await this.mkdirPromise(subPath); 391 | } catch (error) { 392 | if (error.code === `EEXIST`) { 393 | continue; 394 | } else { 395 | throw error; 396 | } 397 | } 398 | createdDirectory ??= subPath; 399 | if (chmod != null) 400 | await this.chmodPromise(subPath, chmod); 401 | if (utimes != null) { 402 | await this.utimesPromise(subPath, utimes[0], utimes[1]); 403 | } else { 404 | const parentStat = await this.statPromise(this.pathUtils.dirname(subPath)); 405 | await this.utimesPromise(subPath, parentStat.atime, parentStat.mtime); 406 | } 407 | } 408 | } 409 | return createdDirectory; 410 | } 411 | mkdirpSync(p, { chmod, utimes } = {}) { 412 | p = this.resolve(p); 413 | if (p === this.pathUtils.dirname(p)) 414 | return void 0; 415 | const parts = p.split(this.pathUtils.sep); 416 | let createdDirectory; 417 | for (let u = 2; u <= parts.length; ++u) { 418 | const subPath = parts.slice(0, u).join(this.pathUtils.sep); 419 | if (!this.existsSync(subPath)) { 420 | try { 421 | this.mkdirSync(subPath); 422 | } catch (error) { 423 | if (error.code === `EEXIST`) { 424 | continue; 425 | } else { 426 | throw error; 427 | } 428 | } 429 | createdDirectory ??= subPath; 430 | if (chmod != null) 431 | this.chmodSync(subPath, chmod); 432 | if (utimes != null) { 433 | this.utimesSync(subPath, utimes[0], utimes[1]); 434 | } else { 435 | const parentStat = this.statSync(this.pathUtils.dirname(subPath)); 436 | this.utimesSync(subPath, parentStat.atime, parentStat.mtime); 437 | } 438 | } 439 | } 440 | return createdDirectory; 441 | } 442 | async copyPromise(destination, source, { baseFs = this, overwrite = true, stableSort = false, stableTime = false, linkStrategy = null } = {}) { 443 | return await copyPromise(this, destination, baseFs, source, { overwrite, stableSort, stableTime, linkStrategy }); 444 | } 445 | copySync(destination, source, { baseFs = this, overwrite = true } = {}) { 446 | const stat = baseFs.lstatSync(source); 447 | const exists = this.existsSync(destination); 448 | if (stat.isDirectory()) { 449 | this.mkdirpSync(destination); 450 | const directoryListing = baseFs.readdirSync(source); 451 | for (const entry of directoryListing) { 452 | this.copySync(this.pathUtils.join(destination, entry), baseFs.pathUtils.join(source, entry), { baseFs, overwrite }); 453 | } 454 | } else if (stat.isFile()) { 455 | if (!exists || overwrite) { 456 | if (exists) 457 | this.removeSync(destination); 458 | const content = baseFs.readFileSync(source); 459 | this.writeFileSync(destination, content); 460 | } 461 | } else if (stat.isSymbolicLink()) { 462 | if (!exists || overwrite) { 463 | if (exists) 464 | this.removeSync(destination); 465 | const target = baseFs.readlinkSync(source); 466 | this.symlinkSync(convertPath(this.pathUtils, target), destination); 467 | } 468 | } else { 469 | throw new Error(`Unsupported file type (file: ${source}, mode: 0o${stat.mode.toString(8).padStart(6, `0`)})`); 470 | } 471 | const mode = stat.mode & 511; 472 | this.chmodSync(destination, mode); 473 | } 474 | async changeFilePromise(p, content, opts = {}) { 475 | if (Buffer.isBuffer(content)) { 476 | return this.changeFileBufferPromise(p, content, opts); 477 | } else { 478 | return this.changeFileTextPromise(p, content, opts); 479 | } 480 | } 481 | async changeFileBufferPromise(p, content, { mode } = {}) { 482 | let current = Buffer.alloc(0); 483 | try { 484 | current = await this.readFilePromise(p); 485 | } catch (error) { 486 | } 487 | if (Buffer.compare(current, content) === 0) 488 | return; 489 | await this.writeFilePromise(p, content, { mode }); 490 | } 491 | async changeFileTextPromise(p, content, { automaticNewlines, mode } = {}) { 492 | let current = ``; 493 | try { 494 | current = await this.readFilePromise(p, `utf8`); 495 | } catch (error) { 496 | } 497 | const normalizedContent = automaticNewlines ? normalizeLineEndings(current, content) : content; 498 | if (current === normalizedContent) 499 | return; 500 | await this.writeFilePromise(p, normalizedContent, { mode }); 501 | } 502 | changeFileSync(p, content, opts = {}) { 503 | if (Buffer.isBuffer(content)) { 504 | return this.changeFileBufferSync(p, content, opts); 505 | } else { 506 | return this.changeFileTextSync(p, content, opts); 507 | } 508 | } 509 | changeFileBufferSync(p, content, { mode } = {}) { 510 | let current = Buffer.alloc(0); 511 | try { 512 | current = this.readFileSync(p); 513 | } catch (error) { 514 | } 515 | if (Buffer.compare(current, content) === 0) 516 | return; 517 | this.writeFileSync(p, content, { mode }); 518 | } 519 | changeFileTextSync(p, content, { automaticNewlines = false, mode } = {}) { 520 | let current = ``; 521 | try { 522 | current = this.readFileSync(p, `utf8`); 523 | } catch (error) { 524 | } 525 | const normalizedContent = automaticNewlines ? normalizeLineEndings(current, content) : content; 526 | if (current === normalizedContent) 527 | return; 528 | this.writeFileSync(p, normalizedContent, { mode }); 529 | } 530 | async movePromise(fromP, toP) { 531 | try { 532 | await this.renamePromise(fromP, toP); 533 | } catch (error) { 534 | if (error.code === `EXDEV`) { 535 | await this.copyPromise(toP, fromP); 536 | await this.removePromise(fromP); 537 | } else { 538 | throw error; 539 | } 540 | } 541 | } 542 | moveSync(fromP, toP) { 543 | try { 544 | this.renameSync(fromP, toP); 545 | } catch (error) { 546 | if (error.code === `EXDEV`) { 547 | this.copySync(toP, fromP); 548 | this.removeSync(fromP); 549 | } else { 550 | throw error; 551 | } 552 | } 553 | } 554 | async lockPromise(affectedPath, callback) { 555 | const lockPath = `${affectedPath}.flock`; 556 | const interval = 1e3 / 60; 557 | const startTime = Date.now(); 558 | let fd = null; 559 | const isAlive = async () => { 560 | let pid; 561 | try { 562 | [pid] = await this.readJsonPromise(lockPath); 563 | } catch (error) { 564 | return Date.now() - startTime < 500; 565 | } 566 | try { 567 | process.kill(pid, 0); 568 | return true; 569 | } catch (error) { 570 | return false; 571 | } 572 | }; 573 | while (fd === null) { 574 | try { 575 | fd = await this.openPromise(lockPath, `wx`); 576 | } catch (error) { 577 | if (error.code === `EEXIST`) { 578 | if (!await isAlive()) { 579 | try { 580 | await this.unlinkPromise(lockPath); 581 | continue; 582 | } catch (error2) { 583 | } 584 | } 585 | if (Date.now() - startTime < 60 * 1e3) { 586 | await new Promise((resolve) => setTimeout(resolve, interval)); 587 | } else { 588 | throw new Error(`Couldn't acquire a lock in a reasonable time (via ${lockPath})`); 589 | } 590 | } else { 591 | throw error; 592 | } 593 | } 594 | } 595 | await this.writePromise(fd, JSON.stringify([process.pid])); 596 | try { 597 | return await callback(); 598 | } finally { 599 | try { 600 | await this.closePromise(fd); 601 | await this.unlinkPromise(lockPath); 602 | } catch (error) { 603 | } 604 | } 605 | } 606 | async readJsonPromise(p) { 607 | const content = await this.readFilePromise(p, `utf8`); 608 | try { 609 | return JSON.parse(content); 610 | } catch (error) { 611 | error.message += ` (in ${p})`; 612 | throw error; 613 | } 614 | } 615 | readJsonSync(p) { 616 | const content = this.readFileSync(p, `utf8`); 617 | try { 618 | return JSON.parse(content); 619 | } catch (error) { 620 | error.message += ` (in ${p})`; 621 | throw error; 622 | } 623 | } 624 | async writeJsonPromise(p, data, { compact = false } = {}) { 625 | const space = compact ? 0 : 2; 626 | return await this.writeFilePromise(p, `${JSON.stringify(data, null, space)} 627 | `); 628 | } 629 | writeJsonSync(p, data, { compact = false } = {}) { 630 | const space = compact ? 0 : 2; 631 | return this.writeFileSync(p, `${JSON.stringify(data, null, space)} 632 | `); 633 | } 634 | async preserveTimePromise(p, cb) { 635 | const stat = await this.lstatPromise(p); 636 | const result = await cb(); 637 | if (typeof result !== `undefined`) 638 | p = result; 639 | await this.lutimesPromise(p, stat.atime, stat.mtime); 640 | } 641 | async preserveTimeSync(p, cb) { 642 | const stat = this.lstatSync(p); 643 | const result = cb(); 644 | if (typeof result !== `undefined`) 645 | p = result; 646 | this.lutimesSync(p, stat.atime, stat.mtime); 647 | } 648 | } 649 | class BasePortableFakeFS extends FakeFS { 650 | constructor() { 651 | super(ppath); 652 | } 653 | } 654 | function getEndOfLine(content) { 655 | const matches = content.match(/\r?\n/g); 656 | if (matches === null) 657 | return EOL; 658 | const crlf = matches.filter((nl) => nl === `\r 659 | `).length; 660 | const lf = matches.length - crlf; 661 | return crlf > lf ? `\r 662 | ` : ` 663 | `; 664 | } 665 | function normalizeLineEndings(originalContent, newContent) { 666 | return newContent.replace(/\r?\n/g, getEndOfLine(originalContent)); 667 | } 668 | 669 | class ProxiedFS extends FakeFS { 670 | getExtractHint(hints) { 671 | return this.baseFs.getExtractHint(hints); 672 | } 673 | resolve(path) { 674 | return this.mapFromBase(this.baseFs.resolve(this.mapToBase(path))); 675 | } 676 | getRealPath() { 677 | return this.mapFromBase(this.baseFs.getRealPath()); 678 | } 679 | async openPromise(p, flags, mode) { 680 | return this.baseFs.openPromise(this.mapToBase(p), flags, mode); 681 | } 682 | openSync(p, flags, mode) { 683 | return this.baseFs.openSync(this.mapToBase(p), flags, mode); 684 | } 685 | async opendirPromise(p, opts) { 686 | return Object.assign(await this.baseFs.opendirPromise(this.mapToBase(p), opts), { path: p }); 687 | } 688 | opendirSync(p, opts) { 689 | return Object.assign(this.baseFs.opendirSync(this.mapToBase(p), opts), { path: p }); 690 | } 691 | async readPromise(fd, buffer, offset, length, position) { 692 | return await this.baseFs.readPromise(fd, buffer, offset, length, position); 693 | } 694 | readSync(fd, buffer, offset, length, position) { 695 | return this.baseFs.readSync(fd, buffer, offset, length, position); 696 | } 697 | async writePromise(fd, buffer, offset, length, position) { 698 | if (typeof buffer === `string`) { 699 | return await this.baseFs.writePromise(fd, buffer, offset); 700 | } else { 701 | return await this.baseFs.writePromise(fd, buffer, offset, length, position); 702 | } 703 | } 704 | writeSync(fd, buffer, offset, length, position) { 705 | if (typeof buffer === `string`) { 706 | return this.baseFs.writeSync(fd, buffer, offset); 707 | } else { 708 | return this.baseFs.writeSync(fd, buffer, offset, length, position); 709 | } 710 | } 711 | async closePromise(fd) { 712 | return this.baseFs.closePromise(fd); 713 | } 714 | closeSync(fd) { 715 | this.baseFs.closeSync(fd); 716 | } 717 | createReadStream(p, opts) { 718 | return this.baseFs.createReadStream(p !== null ? this.mapToBase(p) : p, opts); 719 | } 720 | createWriteStream(p, opts) { 721 | return this.baseFs.createWriteStream(p !== null ? this.mapToBase(p) : p, opts); 722 | } 723 | async realpathPromise(p) { 724 | return this.mapFromBase(await this.baseFs.realpathPromise(this.mapToBase(p))); 725 | } 726 | realpathSync(p) { 727 | return this.mapFromBase(this.baseFs.realpathSync(this.mapToBase(p))); 728 | } 729 | async existsPromise(p) { 730 | return this.baseFs.existsPromise(this.mapToBase(p)); 731 | } 732 | existsSync(p) { 733 | return this.baseFs.existsSync(this.mapToBase(p)); 734 | } 735 | accessSync(p, mode) { 736 | return this.baseFs.accessSync(this.mapToBase(p), mode); 737 | } 738 | async accessPromise(p, mode) { 739 | return this.baseFs.accessPromise(this.mapToBase(p), mode); 740 | } 741 | async statPromise(p, opts) { 742 | return this.baseFs.statPromise(this.mapToBase(p), opts); 743 | } 744 | statSync(p, opts) { 745 | return this.baseFs.statSync(this.mapToBase(p), opts); 746 | } 747 | async fstatPromise(fd, opts) { 748 | return this.baseFs.fstatPromise(fd, opts); 749 | } 750 | fstatSync(fd, opts) { 751 | return this.baseFs.fstatSync(fd, opts); 752 | } 753 | lstatPromise(p, opts) { 754 | return this.baseFs.lstatPromise(this.mapToBase(p), opts); 755 | } 756 | lstatSync(p, opts) { 757 | return this.baseFs.lstatSync(this.mapToBase(p), opts); 758 | } 759 | async fchmodPromise(fd, mask) { 760 | return this.baseFs.fchmodPromise(fd, mask); 761 | } 762 | fchmodSync(fd, mask) { 763 | return this.baseFs.fchmodSync(fd, mask); 764 | } 765 | async chmodPromise(p, mask) { 766 | return this.baseFs.chmodPromise(this.mapToBase(p), mask); 767 | } 768 | chmodSync(p, mask) { 769 | return this.baseFs.chmodSync(this.mapToBase(p), mask); 770 | } 771 | async fchownPromise(fd, uid, gid) { 772 | return this.baseFs.fchownPromise(fd, uid, gid); 773 | } 774 | fchownSync(fd, uid, gid) { 775 | return this.baseFs.fchownSync(fd, uid, gid); 776 | } 777 | async chownPromise(p, uid, gid) { 778 | return this.baseFs.chownPromise(this.mapToBase(p), uid, gid); 779 | } 780 | chownSync(p, uid, gid) { 781 | return this.baseFs.chownSync(this.mapToBase(p), uid, gid); 782 | } 783 | async renamePromise(oldP, newP) { 784 | return this.baseFs.renamePromise(this.mapToBase(oldP), this.mapToBase(newP)); 785 | } 786 | renameSync(oldP, newP) { 787 | return this.baseFs.renameSync(this.mapToBase(oldP), this.mapToBase(newP)); 788 | } 789 | async copyFilePromise(sourceP, destP, flags = 0) { 790 | return this.baseFs.copyFilePromise(this.mapToBase(sourceP), this.mapToBase(destP), flags); 791 | } 792 | copyFileSync(sourceP, destP, flags = 0) { 793 | return this.baseFs.copyFileSync(this.mapToBase(sourceP), this.mapToBase(destP), flags); 794 | } 795 | async appendFilePromise(p, content, opts) { 796 | return this.baseFs.appendFilePromise(this.fsMapToBase(p), content, opts); 797 | } 798 | appendFileSync(p, content, opts) { 799 | return this.baseFs.appendFileSync(this.fsMapToBase(p), content, opts); 800 | } 801 | async writeFilePromise(p, content, opts) { 802 | return this.baseFs.writeFilePromise(this.fsMapToBase(p), content, opts); 803 | } 804 | writeFileSync(p, content, opts) { 805 | return this.baseFs.writeFileSync(this.fsMapToBase(p), content, opts); 806 | } 807 | async unlinkPromise(p) { 808 | return this.baseFs.unlinkPromise(this.mapToBase(p)); 809 | } 810 | unlinkSync(p) { 811 | return this.baseFs.unlinkSync(this.mapToBase(p)); 812 | } 813 | async utimesPromise(p, atime, mtime) { 814 | return this.baseFs.utimesPromise(this.mapToBase(p), atime, mtime); 815 | } 816 | utimesSync(p, atime, mtime) { 817 | return this.baseFs.utimesSync(this.mapToBase(p), atime, mtime); 818 | } 819 | async lutimesPromise(p, atime, mtime) { 820 | return this.baseFs.lutimesPromise(this.mapToBase(p), atime, mtime); 821 | } 822 | lutimesSync(p, atime, mtime) { 823 | return this.baseFs.lutimesSync(this.mapToBase(p), atime, mtime); 824 | } 825 | async mkdirPromise(p, opts) { 826 | return this.baseFs.mkdirPromise(this.mapToBase(p), opts); 827 | } 828 | mkdirSync(p, opts) { 829 | return this.baseFs.mkdirSync(this.mapToBase(p), opts); 830 | } 831 | async rmdirPromise(p, opts) { 832 | return this.baseFs.rmdirPromise(this.mapToBase(p), opts); 833 | } 834 | rmdirSync(p, opts) { 835 | return this.baseFs.rmdirSync(this.mapToBase(p), opts); 836 | } 837 | async rmPromise(p, opts) { 838 | return this.baseFs.rmPromise(this.mapToBase(p), opts); 839 | } 840 | rmSync(p, opts) { 841 | return this.baseFs.rmSync(this.mapToBase(p), opts); 842 | } 843 | async linkPromise(existingP, newP) { 844 | return this.baseFs.linkPromise(this.mapToBase(existingP), this.mapToBase(newP)); 845 | } 846 | linkSync(existingP, newP) { 847 | return this.baseFs.linkSync(this.mapToBase(existingP), this.mapToBase(newP)); 848 | } 849 | async symlinkPromise(target, p, type) { 850 | const mappedP = this.mapToBase(p); 851 | if (this.pathUtils.isAbsolute(target)) 852 | return this.baseFs.symlinkPromise(this.mapToBase(target), mappedP, type); 853 | const mappedAbsoluteTarget = this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(p), target)); 854 | const mappedTarget = this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(mappedP), mappedAbsoluteTarget); 855 | return this.baseFs.symlinkPromise(mappedTarget, mappedP, type); 856 | } 857 | symlinkSync(target, p, type) { 858 | const mappedP = this.mapToBase(p); 859 | if (this.pathUtils.isAbsolute(target)) 860 | return this.baseFs.symlinkSync(this.mapToBase(target), mappedP, type); 861 | const mappedAbsoluteTarget = this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(p), target)); 862 | const mappedTarget = this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(mappedP), mappedAbsoluteTarget); 863 | return this.baseFs.symlinkSync(mappedTarget, mappedP, type); 864 | } 865 | async readFilePromise(p, encoding) { 866 | return this.baseFs.readFilePromise(this.fsMapToBase(p), encoding); 867 | } 868 | readFileSync(p, encoding) { 869 | return this.baseFs.readFileSync(this.fsMapToBase(p), encoding); 870 | } 871 | readdirPromise(p, opts) { 872 | return this.baseFs.readdirPromise(this.mapToBase(p), opts); 873 | } 874 | readdirSync(p, opts) { 875 | return this.baseFs.readdirSync(this.mapToBase(p), opts); 876 | } 877 | async readlinkPromise(p) { 878 | return this.mapFromBase(await this.baseFs.readlinkPromise(this.mapToBase(p))); 879 | } 880 | readlinkSync(p) { 881 | return this.mapFromBase(this.baseFs.readlinkSync(this.mapToBase(p))); 882 | } 883 | async truncatePromise(p, len) { 884 | return this.baseFs.truncatePromise(this.mapToBase(p), len); 885 | } 886 | truncateSync(p, len) { 887 | return this.baseFs.truncateSync(this.mapToBase(p), len); 888 | } 889 | async ftruncatePromise(fd, len) { 890 | return this.baseFs.ftruncatePromise(fd, len); 891 | } 892 | ftruncateSync(fd, len) { 893 | return this.baseFs.ftruncateSync(fd, len); 894 | } 895 | watch(p, a, b) { 896 | return this.baseFs.watch( 897 | this.mapToBase(p), 898 | // @ts-expect-error 899 | a, 900 | b 901 | ); 902 | } 903 | watchFile(p, a, b) { 904 | return this.baseFs.watchFile( 905 | this.mapToBase(p), 906 | // @ts-expect-error 907 | a, 908 | b 909 | ); 910 | } 911 | unwatchFile(p, cb) { 912 | return this.baseFs.unwatchFile(this.mapToBase(p), cb); 913 | } 914 | fsMapToBase(p) { 915 | if (typeof p === `number`) { 916 | return p; 917 | } else { 918 | return this.mapToBase(p); 919 | } 920 | } 921 | } 922 | 923 | function direntToPortable(dirent) { 924 | const portableDirent = dirent; 925 | if (typeof dirent.path === `string`) 926 | portableDirent.path = npath.toPortablePath(dirent.path); 927 | return portableDirent; 928 | } 929 | class NodeFS extends BasePortableFakeFS { 930 | realFs; 931 | constructor(realFs = fs) { 932 | super(); 933 | this.realFs = realFs; 934 | } 935 | getExtractHint() { 936 | return false; 937 | } 938 | getRealPath() { 939 | return PortablePath.root; 940 | } 941 | resolve(p) { 942 | return ppath.resolve(p); 943 | } 944 | async openPromise(p, flags, mode) { 945 | return await new Promise((resolve, reject) => { 946 | this.realFs.open(npath.fromPortablePath(p), flags, mode, this.makeCallback(resolve, reject)); 947 | }); 948 | } 949 | openSync(p, flags, mode) { 950 | return this.realFs.openSync(npath.fromPortablePath(p), flags, mode); 951 | } 952 | async opendirPromise(p, opts) { 953 | return await new Promise((resolve, reject) => { 954 | if (typeof opts !== `undefined`) { 955 | this.realFs.opendir(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject)); 956 | } else { 957 | this.realFs.opendir(npath.fromPortablePath(p), this.makeCallback(resolve, reject)); 958 | } 959 | }).then((dir) => { 960 | const dirWithFixedPath = dir; 961 | Object.defineProperty(dirWithFixedPath, `path`, { 962 | value: p, 963 | configurable: true, 964 | writable: true 965 | }); 966 | return dirWithFixedPath; 967 | }); 968 | } 969 | opendirSync(p, opts) { 970 | const dir = typeof opts !== `undefined` ? this.realFs.opendirSync(npath.fromPortablePath(p), opts) : this.realFs.opendirSync(npath.fromPortablePath(p)); 971 | const dirWithFixedPath = dir; 972 | Object.defineProperty(dirWithFixedPath, `path`, { 973 | value: p, 974 | configurable: true, 975 | writable: true 976 | }); 977 | return dirWithFixedPath; 978 | } 979 | async readPromise(fd, buffer, offset = 0, length = 0, position = -1) { 980 | return await new Promise((resolve, reject) => { 981 | this.realFs.read(fd, buffer, offset, length, position, (error, bytesRead) => { 982 | if (error) { 983 | reject(error); 984 | } else { 985 | resolve(bytesRead); 986 | } 987 | }); 988 | }); 989 | } 990 | readSync(fd, buffer, offset, length, position) { 991 | return this.realFs.readSync(fd, buffer, offset, length, position); 992 | } 993 | async writePromise(fd, buffer, offset, length, position) { 994 | return await new Promise((resolve, reject) => { 995 | if (typeof buffer === `string`) { 996 | return this.realFs.write(fd, buffer, offset, this.makeCallback(resolve, reject)); 997 | } else { 998 | return this.realFs.write(fd, buffer, offset, length, position, this.makeCallback(resolve, reject)); 999 | } 1000 | }); 1001 | } 1002 | writeSync(fd, buffer, offset, length, position) { 1003 | if (typeof buffer === `string`) { 1004 | return this.realFs.writeSync(fd, buffer, offset); 1005 | } else { 1006 | return this.realFs.writeSync(fd, buffer, offset, length, position); 1007 | } 1008 | } 1009 | async closePromise(fd) { 1010 | await new Promise((resolve, reject) => { 1011 | this.realFs.close(fd, this.makeCallback(resolve, reject)); 1012 | }); 1013 | } 1014 | closeSync(fd) { 1015 | this.realFs.closeSync(fd); 1016 | } 1017 | createReadStream(p, opts) { 1018 | const realPath = p !== null ? npath.fromPortablePath(p) : p; 1019 | return this.realFs.createReadStream(realPath, opts); 1020 | } 1021 | createWriteStream(p, opts) { 1022 | const realPath = p !== null ? npath.fromPortablePath(p) : p; 1023 | return this.realFs.createWriteStream(realPath, opts); 1024 | } 1025 | async realpathPromise(p) { 1026 | return await new Promise((resolve, reject) => { 1027 | this.realFs.realpath(npath.fromPortablePath(p), {}, this.makeCallback(resolve, reject)); 1028 | }).then((path) => { 1029 | return npath.toPortablePath(path); 1030 | }); 1031 | } 1032 | realpathSync(p) { 1033 | return npath.toPortablePath(this.realFs.realpathSync(npath.fromPortablePath(p), {})); 1034 | } 1035 | async existsPromise(p) { 1036 | return await new Promise((resolve) => { 1037 | this.realFs.exists(npath.fromPortablePath(p), resolve); 1038 | }); 1039 | } 1040 | accessSync(p, mode) { 1041 | return this.realFs.accessSync(npath.fromPortablePath(p), mode); 1042 | } 1043 | async accessPromise(p, mode) { 1044 | return await new Promise((resolve, reject) => { 1045 | this.realFs.access(npath.fromPortablePath(p), mode, this.makeCallback(resolve, reject)); 1046 | }); 1047 | } 1048 | existsSync(p) { 1049 | return this.realFs.existsSync(npath.fromPortablePath(p)); 1050 | } 1051 | async statPromise(p, opts) { 1052 | return await new Promise((resolve, reject) => { 1053 | if (opts) { 1054 | this.realFs.stat(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject)); 1055 | } else { 1056 | this.realFs.stat(npath.fromPortablePath(p), this.makeCallback(resolve, reject)); 1057 | } 1058 | }); 1059 | } 1060 | statSync(p, opts) { 1061 | if (opts) { 1062 | return this.realFs.statSync(npath.fromPortablePath(p), opts); 1063 | } else { 1064 | return this.realFs.statSync(npath.fromPortablePath(p)); 1065 | } 1066 | } 1067 | async fstatPromise(fd, opts) { 1068 | return await new Promise((resolve, reject) => { 1069 | if (opts) { 1070 | this.realFs.fstat(fd, opts, this.makeCallback(resolve, reject)); 1071 | } else { 1072 | this.realFs.fstat(fd, this.makeCallback(resolve, reject)); 1073 | } 1074 | }); 1075 | } 1076 | fstatSync(fd, opts) { 1077 | if (opts) { 1078 | return this.realFs.fstatSync(fd, opts); 1079 | } else { 1080 | return this.realFs.fstatSync(fd); 1081 | } 1082 | } 1083 | async lstatPromise(p, opts) { 1084 | return await new Promise((resolve, reject) => { 1085 | if (opts) { 1086 | this.realFs.lstat(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject)); 1087 | } else { 1088 | this.realFs.lstat(npath.fromPortablePath(p), this.makeCallback(resolve, reject)); 1089 | } 1090 | }); 1091 | } 1092 | lstatSync(p, opts) { 1093 | if (opts) { 1094 | return this.realFs.lstatSync(npath.fromPortablePath(p), opts); 1095 | } else { 1096 | return this.realFs.lstatSync(npath.fromPortablePath(p)); 1097 | } 1098 | } 1099 | async fchmodPromise(fd, mask) { 1100 | return await new Promise((resolve, reject) => { 1101 | this.realFs.fchmod(fd, mask, this.makeCallback(resolve, reject)); 1102 | }); 1103 | } 1104 | fchmodSync(fd, mask) { 1105 | return this.realFs.fchmodSync(fd, mask); 1106 | } 1107 | async chmodPromise(p, mask) { 1108 | return await new Promise((resolve, reject) => { 1109 | this.realFs.chmod(npath.fromPortablePath(p), mask, this.makeCallback(resolve, reject)); 1110 | }); 1111 | } 1112 | chmodSync(p, mask) { 1113 | return this.realFs.chmodSync(npath.fromPortablePath(p), mask); 1114 | } 1115 | async fchownPromise(fd, uid, gid) { 1116 | return await new Promise((resolve, reject) => { 1117 | this.realFs.fchown(fd, uid, gid, this.makeCallback(resolve, reject)); 1118 | }); 1119 | } 1120 | fchownSync(fd, uid, gid) { 1121 | return this.realFs.fchownSync(fd, uid, gid); 1122 | } 1123 | async chownPromise(p, uid, gid) { 1124 | return await new Promise((resolve, reject) => { 1125 | this.realFs.chown(npath.fromPortablePath(p), uid, gid, this.makeCallback(resolve, reject)); 1126 | }); 1127 | } 1128 | chownSync(p, uid, gid) { 1129 | return this.realFs.chownSync(npath.fromPortablePath(p), uid, gid); 1130 | } 1131 | async renamePromise(oldP, newP) { 1132 | return await new Promise((resolve, reject) => { 1133 | this.realFs.rename(npath.fromPortablePath(oldP), npath.fromPortablePath(newP), this.makeCallback(resolve, reject)); 1134 | }); 1135 | } 1136 | renameSync(oldP, newP) { 1137 | return this.realFs.renameSync(npath.fromPortablePath(oldP), npath.fromPortablePath(newP)); 1138 | } 1139 | async copyFilePromise(sourceP, destP, flags = 0) { 1140 | return await new Promise((resolve, reject) => { 1141 | this.realFs.copyFile(npath.fromPortablePath(sourceP), npath.fromPortablePath(destP), flags, this.makeCallback(resolve, reject)); 1142 | }); 1143 | } 1144 | copyFileSync(sourceP, destP, flags = 0) { 1145 | return this.realFs.copyFileSync(npath.fromPortablePath(sourceP), npath.fromPortablePath(destP), flags); 1146 | } 1147 | async appendFilePromise(p, content, opts) { 1148 | return await new Promise((resolve, reject) => { 1149 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p; 1150 | if (opts) { 1151 | this.realFs.appendFile(fsNativePath, content, opts, this.makeCallback(resolve, reject)); 1152 | } else { 1153 | this.realFs.appendFile(fsNativePath, content, this.makeCallback(resolve, reject)); 1154 | } 1155 | }); 1156 | } 1157 | appendFileSync(p, content, opts) { 1158 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p; 1159 | if (opts) { 1160 | this.realFs.appendFileSync(fsNativePath, content, opts); 1161 | } else { 1162 | this.realFs.appendFileSync(fsNativePath, content); 1163 | } 1164 | } 1165 | async writeFilePromise(p, content, opts) { 1166 | return await new Promise((resolve, reject) => { 1167 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p; 1168 | if (opts) { 1169 | this.realFs.writeFile(fsNativePath, content, opts, this.makeCallback(resolve, reject)); 1170 | } else { 1171 | this.realFs.writeFile(fsNativePath, content, this.makeCallback(resolve, reject)); 1172 | } 1173 | }); 1174 | } 1175 | writeFileSync(p, content, opts) { 1176 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p; 1177 | if (opts) { 1178 | this.realFs.writeFileSync(fsNativePath, content, opts); 1179 | } else { 1180 | this.realFs.writeFileSync(fsNativePath, content); 1181 | } 1182 | } 1183 | async unlinkPromise(p) { 1184 | return await new Promise((resolve, reject) => { 1185 | this.realFs.unlink(npath.fromPortablePath(p), this.makeCallback(resolve, reject)); 1186 | }); 1187 | } 1188 | unlinkSync(p) { 1189 | return this.realFs.unlinkSync(npath.fromPortablePath(p)); 1190 | } 1191 | async utimesPromise(p, atime, mtime) { 1192 | return await new Promise((resolve, reject) => { 1193 | this.realFs.utimes(npath.fromPortablePath(p), atime, mtime, this.makeCallback(resolve, reject)); 1194 | }); 1195 | } 1196 | utimesSync(p, atime, mtime) { 1197 | this.realFs.utimesSync(npath.fromPortablePath(p), atime, mtime); 1198 | } 1199 | async lutimesPromise(p, atime, mtime) { 1200 | return await new Promise((resolve, reject) => { 1201 | this.realFs.lutimes(npath.fromPortablePath(p), atime, mtime, this.makeCallback(resolve, reject)); 1202 | }); 1203 | } 1204 | lutimesSync(p, atime, mtime) { 1205 | this.realFs.lutimesSync(npath.fromPortablePath(p), atime, mtime); 1206 | } 1207 | async mkdirPromise(p, opts) { 1208 | return await new Promise((resolve, reject) => { 1209 | this.realFs.mkdir(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject)); 1210 | }); 1211 | } 1212 | mkdirSync(p, opts) { 1213 | return this.realFs.mkdirSync(npath.fromPortablePath(p), opts); 1214 | } 1215 | async rmdirPromise(p, opts) { 1216 | return await new Promise((resolve, reject) => { 1217 | if (opts) { 1218 | this.realFs.rmdir(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject)); 1219 | } else { 1220 | this.realFs.rmdir(npath.fromPortablePath(p), this.makeCallback(resolve, reject)); 1221 | } 1222 | }); 1223 | } 1224 | rmdirSync(p, opts) { 1225 | return this.realFs.rmdirSync(npath.fromPortablePath(p), opts); 1226 | } 1227 | async rmPromise(p, opts) { 1228 | return await new Promise((resolve, reject) => { 1229 | if (opts) { 1230 | this.realFs.rm(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject)); 1231 | } else { 1232 | this.realFs.rm(npath.fromPortablePath(p), this.makeCallback(resolve, reject)); 1233 | } 1234 | }); 1235 | } 1236 | rmSync(p, opts) { 1237 | return this.realFs.rmSync(npath.fromPortablePath(p), opts); 1238 | } 1239 | async linkPromise(existingP, newP) { 1240 | return await new Promise((resolve, reject) => { 1241 | this.realFs.link(npath.fromPortablePath(existingP), npath.fromPortablePath(newP), this.makeCallback(resolve, reject)); 1242 | }); 1243 | } 1244 | linkSync(existingP, newP) { 1245 | return this.realFs.linkSync(npath.fromPortablePath(existingP), npath.fromPortablePath(newP)); 1246 | } 1247 | async symlinkPromise(target, p, type) { 1248 | return await new Promise((resolve, reject) => { 1249 | this.realFs.symlink(npath.fromPortablePath(target.replace(/\/+$/, ``)), npath.fromPortablePath(p), type, this.makeCallback(resolve, reject)); 1250 | }); 1251 | } 1252 | symlinkSync(target, p, type) { 1253 | return this.realFs.symlinkSync(npath.fromPortablePath(target.replace(/\/+$/, ``)), npath.fromPortablePath(p), type); 1254 | } 1255 | async readFilePromise(p, encoding) { 1256 | return await new Promise((resolve, reject) => { 1257 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p; 1258 | this.realFs.readFile(fsNativePath, encoding, this.makeCallback(resolve, reject)); 1259 | }); 1260 | } 1261 | readFileSync(p, encoding) { 1262 | const fsNativePath = typeof p === `string` ? npath.fromPortablePath(p) : p; 1263 | return this.realFs.readFileSync(fsNativePath, encoding); 1264 | } 1265 | async readdirPromise(p, opts) { 1266 | return await new Promise((resolve, reject) => { 1267 | if (opts) { 1268 | if (opts.recursive && process.platform === `win32`) { 1269 | if (opts.withFileTypes) { 1270 | this.realFs.readdir(npath.fromPortablePath(p), opts, this.makeCallback((results) => resolve(results.map(direntToPortable)), reject)); 1271 | } else { 1272 | this.realFs.readdir(npath.fromPortablePath(p), opts, this.makeCallback((results) => resolve(results.map(npath.toPortablePath)), reject)); 1273 | } 1274 | } else { 1275 | this.realFs.readdir(npath.fromPortablePath(p), opts, this.makeCallback(resolve, reject)); 1276 | } 1277 | } else { 1278 | this.realFs.readdir(npath.fromPortablePath(p), this.makeCallback(resolve, reject)); 1279 | } 1280 | }); 1281 | } 1282 | readdirSync(p, opts) { 1283 | if (opts) { 1284 | if (opts.recursive && process.platform === `win32`) { 1285 | if (opts.withFileTypes) { 1286 | return this.realFs.readdirSync(npath.fromPortablePath(p), opts).map(direntToPortable); 1287 | } else { 1288 | return this.realFs.readdirSync(npath.fromPortablePath(p), opts).map(npath.toPortablePath); 1289 | } 1290 | } else { 1291 | return this.realFs.readdirSync(npath.fromPortablePath(p), opts); 1292 | } 1293 | } else { 1294 | return this.realFs.readdirSync(npath.fromPortablePath(p)); 1295 | } 1296 | } 1297 | async readlinkPromise(p) { 1298 | return await new Promise((resolve, reject) => { 1299 | this.realFs.readlink(npath.fromPortablePath(p), this.makeCallback(resolve, reject)); 1300 | }).then((path) => { 1301 | return npath.toPortablePath(path); 1302 | }); 1303 | } 1304 | readlinkSync(p) { 1305 | return npath.toPortablePath(this.realFs.readlinkSync(npath.fromPortablePath(p))); 1306 | } 1307 | async truncatePromise(p, len) { 1308 | return await new Promise((resolve, reject) => { 1309 | this.realFs.truncate(npath.fromPortablePath(p), len, this.makeCallback(resolve, reject)); 1310 | }); 1311 | } 1312 | truncateSync(p, len) { 1313 | return this.realFs.truncateSync(npath.fromPortablePath(p), len); 1314 | } 1315 | async ftruncatePromise(fd, len) { 1316 | return await new Promise((resolve, reject) => { 1317 | this.realFs.ftruncate(fd, len, this.makeCallback(resolve, reject)); 1318 | }); 1319 | } 1320 | ftruncateSync(fd, len) { 1321 | return this.realFs.ftruncateSync(fd, len); 1322 | } 1323 | watch(p, a, b) { 1324 | return this.realFs.watch( 1325 | npath.fromPortablePath(p), 1326 | // @ts-expect-error 1327 | a, 1328 | b 1329 | ); 1330 | } 1331 | watchFile(p, a, b) { 1332 | return this.realFs.watchFile( 1333 | npath.fromPortablePath(p), 1334 | // @ts-expect-error 1335 | a, 1336 | b 1337 | ); 1338 | } 1339 | unwatchFile(p, cb) { 1340 | return this.realFs.unwatchFile(npath.fromPortablePath(p), cb); 1341 | } 1342 | makeCallback(resolve, reject) { 1343 | return (err, result) => { 1344 | if (err) { 1345 | reject(err); 1346 | } else { 1347 | resolve(result); 1348 | } 1349 | }; 1350 | } 1351 | } 1352 | 1353 | const NUMBER_REGEXP = /^[0-9]+$/; 1354 | const VIRTUAL_REGEXP = /^(\/(?:[^/]+\/)*?(?:\$\$virtual|__virtual__))((?:\/((?:[^/]+-)?[a-f0-9]+)(?:\/([^/]+))?)?((?:\/.*)?))$/; 1355 | const VALID_COMPONENT = /^([^/]+-)?[a-f0-9]+$/; 1356 | class VirtualFS extends ProxiedFS { 1357 | baseFs; 1358 | static makeVirtualPath(base, component, to) { 1359 | if (ppath.basename(base) !== `__virtual__`) 1360 | throw new Error(`Assertion failed: Virtual folders must be named "__virtual__"`); 1361 | if (!ppath.basename(component).match(VALID_COMPONENT)) 1362 | throw new Error(`Assertion failed: Virtual components must be ended by an hexadecimal hash`); 1363 | const target = ppath.relative(ppath.dirname(base), to); 1364 | const segments = target.split(`/`); 1365 | let depth = 0; 1366 | while (depth < segments.length && segments[depth] === `..`) 1367 | depth += 1; 1368 | const finalSegments = segments.slice(depth); 1369 | const fullVirtualPath = ppath.join(base, component, String(depth), ...finalSegments); 1370 | return fullVirtualPath; 1371 | } 1372 | static resolveVirtual(p) { 1373 | const match = p.match(VIRTUAL_REGEXP); 1374 | if (!match || !match[3] && match[5]) 1375 | return p; 1376 | const target = ppath.dirname(match[1]); 1377 | if (!match[3] || !match[4]) 1378 | return target; 1379 | const isnum = NUMBER_REGEXP.test(match[4]); 1380 | if (!isnum) 1381 | return p; 1382 | const depth = Number(match[4]); 1383 | const backstep = `../`.repeat(depth); 1384 | const subpath = match[5] || `.`; 1385 | return VirtualFS.resolveVirtual(ppath.join(target, backstep, subpath)); 1386 | } 1387 | constructor({ baseFs = new NodeFS() } = {}) { 1388 | super(ppath); 1389 | this.baseFs = baseFs; 1390 | } 1391 | getExtractHint(hints) { 1392 | return this.baseFs.getExtractHint(hints); 1393 | } 1394 | getRealPath() { 1395 | return this.baseFs.getRealPath(); 1396 | } 1397 | realpathSync(p) { 1398 | const match = p.match(VIRTUAL_REGEXP); 1399 | if (!match) 1400 | return this.baseFs.realpathSync(p); 1401 | if (!match[5]) 1402 | return p; 1403 | const realpath = this.baseFs.realpathSync(this.mapToBase(p)); 1404 | return VirtualFS.makeVirtualPath(match[1], match[3], realpath); 1405 | } 1406 | async realpathPromise(p) { 1407 | const match = p.match(VIRTUAL_REGEXP); 1408 | if (!match) 1409 | return await this.baseFs.realpathPromise(p); 1410 | if (!match[5]) 1411 | return p; 1412 | const realpath = await this.baseFs.realpathPromise(this.mapToBase(p)); 1413 | return VirtualFS.makeVirtualPath(match[1], match[3], realpath); 1414 | } 1415 | mapToBase(p) { 1416 | if (p === ``) 1417 | return p; 1418 | if (this.pathUtils.isAbsolute(p)) 1419 | return VirtualFS.resolveVirtual(p); 1420 | const resolvedRoot = VirtualFS.resolveVirtual(this.baseFs.resolve(PortablePath.dot)); 1421 | const resolvedP = VirtualFS.resolveVirtual(this.baseFs.resolve(p)); 1422 | return ppath.relative(resolvedRoot, resolvedP) || PortablePath.dot; 1423 | } 1424 | mapFromBase(p) { 1425 | return p; 1426 | } 1427 | } 1428 | 1429 | const URL = Number(process.versions.node.split('.', 1)[0]) < 20 ? URL$1 : globalThis.URL; 1430 | 1431 | const [major, minor] = process.versions.node.split(`.`).map((value) => parseInt(value, 10)); 1432 | const WATCH_MODE_MESSAGE_USES_ARRAYS = major > 19 || major === 19 && minor >= 2 || major === 18 && minor >= 13; 1433 | const HAS_LAZY_LOADED_TRANSLATORS = major === 20 && minor < 6 || major === 19 && minor >= 3; 1434 | const SUPPORTS_IMPORT_ATTRIBUTES = major >= 21 || major === 20 && minor >= 10 || major === 18 && minor >= 20; 1435 | const SUPPORTS_IMPORT_ATTRIBUTES_ONLY = major >= 22; 1436 | 1437 | function readPackageScope(checkPath) { 1438 | const rootSeparatorIndex = checkPath.indexOf(npath.sep); 1439 | let separatorIndex; 1440 | do { 1441 | separatorIndex = checkPath.lastIndexOf(npath.sep); 1442 | checkPath = checkPath.slice(0, separatorIndex); 1443 | if (checkPath.endsWith(`${npath.sep}node_modules`)) 1444 | return false; 1445 | const pjson = readPackage(checkPath + npath.sep); 1446 | if (pjson) { 1447 | return { 1448 | data: pjson, 1449 | path: checkPath 1450 | }; 1451 | } 1452 | } while (separatorIndex > rootSeparatorIndex); 1453 | return false; 1454 | } 1455 | function readPackage(requestPath) { 1456 | const jsonPath = npath.resolve(requestPath, `package.json`); 1457 | if (!fs.existsSync(jsonPath)) 1458 | return null; 1459 | return JSON.parse(fs.readFileSync(jsonPath, `utf8`)); 1460 | } 1461 | 1462 | async function tryReadFile$1(path2) { 1463 | try { 1464 | return await fs.promises.readFile(path2, `utf8`); 1465 | } catch (error) { 1466 | if (error.code === `ENOENT`) 1467 | return null; 1468 | throw error; 1469 | } 1470 | } 1471 | function tryParseURL(str, base) { 1472 | try { 1473 | return new URL(str, base); 1474 | } catch { 1475 | return null; 1476 | } 1477 | } 1478 | let entrypointPath = null; 1479 | function setEntrypointPath(file) { 1480 | entrypointPath = file; 1481 | } 1482 | function getFileFormat(filepath) { 1483 | const ext = path.extname(filepath); 1484 | switch (ext) { 1485 | case `.mjs`: { 1486 | return `module`; 1487 | } 1488 | case `.cjs`: { 1489 | return `commonjs`; 1490 | } 1491 | case `.wasm`: { 1492 | throw new Error( 1493 | `Unknown file extension ".wasm" for ${filepath}` 1494 | ); 1495 | } 1496 | case `.json`: { 1497 | return `json`; 1498 | } 1499 | case `.js`: { 1500 | const pkg = readPackageScope(filepath); 1501 | if (!pkg) 1502 | return `commonjs`; 1503 | return pkg.data.type ?? `commonjs`; 1504 | } 1505 | default: { 1506 | if (entrypointPath !== filepath) 1507 | return null; 1508 | const pkg = readPackageScope(filepath); 1509 | if (!pkg) 1510 | return `commonjs`; 1511 | if (pkg.data.type === `module`) 1512 | return null; 1513 | return pkg.data.type ?? `commonjs`; 1514 | } 1515 | } 1516 | } 1517 | 1518 | async function load$1(urlString, context, nextLoad) { 1519 | const url = tryParseURL(urlString); 1520 | if (url?.protocol !== `file:`) 1521 | return nextLoad(urlString, context, nextLoad); 1522 | const filePath = fileURLToPath(url); 1523 | const format = getFileFormat(filePath); 1524 | if (!format) 1525 | return nextLoad(urlString, context, nextLoad); 1526 | if (format === `json`) { 1527 | if (SUPPORTS_IMPORT_ATTRIBUTES_ONLY) { 1528 | if (context.importAttributes?.type !== `json`) { 1529 | const err = new TypeError(`[ERR_IMPORT_ATTRIBUTE_MISSING]: Module "${urlString}" needs an import attribute of "type: json"`); 1530 | err.code = `ERR_IMPORT_ATTRIBUTE_MISSING`; 1531 | throw err; 1532 | } 1533 | } else { 1534 | const type = `importAttributes` in context ? context.importAttributes?.type : context.importAssertions?.type; 1535 | if (type !== `json`) { 1536 | const err = new TypeError(`[ERR_IMPORT_ASSERTION_TYPE_MISSING]: Module "${urlString}" needs an import ${SUPPORTS_IMPORT_ATTRIBUTES ? `attribute` : `assertion`} of type "json"`); 1537 | err.code = `ERR_IMPORT_ASSERTION_TYPE_MISSING`; 1538 | throw err; 1539 | } 1540 | } 1541 | } 1542 | if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { 1543 | const pathToSend = pathToFileURL( 1544 | npath.fromPortablePath( 1545 | VirtualFS.resolveVirtual(npath.toPortablePath(filePath)) 1546 | ) 1547 | ).href; 1548 | process.send({ 1549 | "watch:import": WATCH_MODE_MESSAGE_USES_ARRAYS ? [pathToSend] : pathToSend 1550 | }); 1551 | } 1552 | return { 1553 | format, 1554 | source: format === `commonjs` ? void 0 : await fs.promises.readFile(filePath, `utf8`), 1555 | shortCircuit: true 1556 | }; 1557 | } 1558 | 1559 | const ArrayIsArray = Array.isArray; 1560 | const JSONStringify = JSON.stringify; 1561 | const ObjectGetOwnPropertyNames = Object.getOwnPropertyNames; 1562 | const ObjectPrototypeHasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); 1563 | const RegExpPrototypeExec = (obj, string) => RegExp.prototype.exec.call(obj, string); 1564 | const RegExpPrototypeSymbolReplace = (obj, ...rest) => RegExp.prototype[Symbol.replace].apply(obj, rest); 1565 | const StringPrototypeEndsWith = (str, ...rest) => String.prototype.endsWith.apply(str, rest); 1566 | const StringPrototypeIncludes = (str, ...rest) => String.prototype.includes.apply(str, rest); 1567 | const StringPrototypeLastIndexOf = (str, ...rest) => String.prototype.lastIndexOf.apply(str, rest); 1568 | const StringPrototypeIndexOf = (str, ...rest) => String.prototype.indexOf.apply(str, rest); 1569 | const StringPrototypeReplace = (str, ...rest) => String.prototype.replace.apply(str, rest); 1570 | const StringPrototypeSlice = (str, ...rest) => String.prototype.slice.apply(str, rest); 1571 | const StringPrototypeStartsWith = (str, ...rest) => String.prototype.startsWith.apply(str, rest); 1572 | const SafeMap = Map; 1573 | const JSONParse = JSON.parse; 1574 | 1575 | function createErrorType(code, messageCreator, errorType) { 1576 | return class extends errorType { 1577 | constructor(...args) { 1578 | super(messageCreator(...args)); 1579 | this.code = code; 1580 | this.name = `${errorType.name} [${code}]`; 1581 | } 1582 | }; 1583 | } 1584 | const ERR_PACKAGE_IMPORT_NOT_DEFINED = createErrorType( 1585 | `ERR_PACKAGE_IMPORT_NOT_DEFINED`, 1586 | (specifier, packagePath, base) => { 1587 | return `Package import specifier "${specifier}" is not defined${packagePath ? ` in package ${packagePath}package.json` : ``} imported from ${base}`; 1588 | }, 1589 | TypeError 1590 | ); 1591 | const ERR_INVALID_MODULE_SPECIFIER = createErrorType( 1592 | `ERR_INVALID_MODULE_SPECIFIER`, 1593 | (request, reason, base = void 0) => { 1594 | return `Invalid module "${request}" ${reason}${base ? ` imported from ${base}` : ``}`; 1595 | }, 1596 | TypeError 1597 | ); 1598 | const ERR_INVALID_PACKAGE_TARGET = createErrorType( 1599 | `ERR_INVALID_PACKAGE_TARGET`, 1600 | (pkgPath, key, target, isImport = false, base = void 0) => { 1601 | const relError = typeof target === `string` && !isImport && target.length && !StringPrototypeStartsWith(target, `./`); 1602 | if (key === `.`) { 1603 | assert(isImport === false); 1604 | return `Invalid "exports" main target ${JSONStringify(target)} defined in the package config ${pkgPath}package.json${base ? ` imported from ${base}` : ``}${relError ? `; targets must start with "./"` : ``}`; 1605 | } 1606 | return `Invalid "${isImport ? `imports` : `exports`}" target ${JSONStringify( 1607 | target 1608 | )} defined for '${key}' in the package config ${pkgPath}package.json${base ? ` imported from ${base}` : ``}${relError ? `; targets must start with "./"` : ``}`; 1609 | }, 1610 | Error 1611 | ); 1612 | const ERR_INVALID_PACKAGE_CONFIG = createErrorType( 1613 | `ERR_INVALID_PACKAGE_CONFIG`, 1614 | (path, base, message) => { 1615 | return `Invalid package config ${path}${base ? ` while importing ${base}` : ``}${message ? `. ${message}` : ``}`; 1616 | }, 1617 | Error 1618 | ); 1619 | 1620 | function filterOwnProperties(source, keys) { 1621 | const filtered = /* @__PURE__ */ Object.create(null); 1622 | for (let i = 0; i < keys.length; i++) { 1623 | const key = keys[i]; 1624 | if (ObjectPrototypeHasOwnProperty(source, key)) { 1625 | filtered[key] = source[key]; 1626 | } 1627 | } 1628 | return filtered; 1629 | } 1630 | 1631 | const packageJSONCache = new SafeMap(); 1632 | function getPackageConfig(path, specifier, base, readFileSyncFn) { 1633 | const existing = packageJSONCache.get(path); 1634 | if (existing !== void 0) { 1635 | return existing; 1636 | } 1637 | const source = readFileSyncFn(path); 1638 | if (source === void 0) { 1639 | const packageConfig2 = { 1640 | pjsonPath: path, 1641 | exists: false, 1642 | main: void 0, 1643 | name: void 0, 1644 | type: "none", 1645 | exports: void 0, 1646 | imports: void 0 1647 | }; 1648 | packageJSONCache.set(path, packageConfig2); 1649 | return packageConfig2; 1650 | } 1651 | let packageJSON; 1652 | try { 1653 | packageJSON = JSONParse(source); 1654 | } catch (error) { 1655 | throw new ERR_INVALID_PACKAGE_CONFIG( 1656 | path, 1657 | (base ? `"${specifier}" from ` : "") + fileURLToPath(base || specifier), 1658 | error.message 1659 | ); 1660 | } 1661 | let { imports, main, name, type } = filterOwnProperties(packageJSON, [ 1662 | "imports", 1663 | "main", 1664 | "name", 1665 | "type" 1666 | ]); 1667 | const exports = ObjectPrototypeHasOwnProperty(packageJSON, "exports") ? packageJSON.exports : void 0; 1668 | if (typeof imports !== "object" || imports === null) { 1669 | imports = void 0; 1670 | } 1671 | if (typeof main !== "string") { 1672 | main = void 0; 1673 | } 1674 | if (typeof name !== "string") { 1675 | name = void 0; 1676 | } 1677 | if (type !== "module" && type !== "commonjs") { 1678 | type = "none"; 1679 | } 1680 | const packageConfig = { 1681 | pjsonPath: path, 1682 | exists: true, 1683 | main, 1684 | name, 1685 | type, 1686 | exports, 1687 | imports 1688 | }; 1689 | packageJSONCache.set(path, packageConfig); 1690 | return packageConfig; 1691 | } 1692 | function getPackageScopeConfig(resolved, readFileSyncFn) { 1693 | let packageJSONUrl = new URL("./package.json", resolved); 1694 | while (true) { 1695 | const packageJSONPath2 = packageJSONUrl.pathname; 1696 | if (StringPrototypeEndsWith(packageJSONPath2, "node_modules/package.json")) { 1697 | break; 1698 | } 1699 | const packageConfig2 = getPackageConfig( 1700 | fileURLToPath(packageJSONUrl), 1701 | resolved, 1702 | void 0, 1703 | readFileSyncFn 1704 | ); 1705 | if (packageConfig2.exists) { 1706 | return packageConfig2; 1707 | } 1708 | const lastPackageJSONUrl = packageJSONUrl; 1709 | packageJSONUrl = new URL("../package.json", packageJSONUrl); 1710 | if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) { 1711 | break; 1712 | } 1713 | } 1714 | const packageJSONPath = fileURLToPath(packageJSONUrl); 1715 | const packageConfig = { 1716 | pjsonPath: packageJSONPath, 1717 | exists: false, 1718 | main: void 0, 1719 | name: void 0, 1720 | type: "none", 1721 | exports: void 0, 1722 | imports: void 0 1723 | }; 1724 | packageJSONCache.set(packageJSONPath, packageConfig); 1725 | return packageConfig; 1726 | } 1727 | 1728 | function throwImportNotDefined(specifier, packageJSONUrl, base) { 1729 | throw new ERR_PACKAGE_IMPORT_NOT_DEFINED( 1730 | specifier, 1731 | packageJSONUrl && fileURLToPath(new URL(".", packageJSONUrl)), 1732 | fileURLToPath(base) 1733 | ); 1734 | } 1735 | function throwInvalidSubpath(subpath, packageJSONUrl, internal, base) { 1736 | const reason = `request is not a valid subpath for the "${internal ? "imports" : "exports"}" resolution of ${fileURLToPath(packageJSONUrl)}`; 1737 | throw new ERR_INVALID_MODULE_SPECIFIER( 1738 | subpath, 1739 | reason, 1740 | base && fileURLToPath(base) 1741 | ); 1742 | } 1743 | function throwInvalidPackageTarget(subpath, target, packageJSONUrl, internal, base) { 1744 | if (typeof target === "object" && target !== null) { 1745 | target = JSONStringify(target, null, ""); 1746 | } else { 1747 | target = `${target}`; 1748 | } 1749 | throw new ERR_INVALID_PACKAGE_TARGET( 1750 | fileURLToPath(new URL(".", packageJSONUrl)), 1751 | subpath, 1752 | target, 1753 | internal, 1754 | base && fileURLToPath(base) 1755 | ); 1756 | } 1757 | const invalidSegmentRegEx = /(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i; 1758 | const patternRegEx = /\*/g; 1759 | function resolvePackageTargetString(target, subpath, match, packageJSONUrl, base, pattern, internal, conditions) { 1760 | if (subpath !== "" && !pattern && target[target.length - 1] !== "/") 1761 | throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); 1762 | if (!StringPrototypeStartsWith(target, "./")) { 1763 | if (internal && !StringPrototypeStartsWith(target, "../") && !StringPrototypeStartsWith(target, "/")) { 1764 | let isURL = false; 1765 | try { 1766 | new URL(target); 1767 | isURL = true; 1768 | } catch { 1769 | } 1770 | if (!isURL) { 1771 | const exportTarget = pattern ? RegExpPrototypeSymbolReplace(patternRegEx, target, () => subpath) : target + subpath; 1772 | return exportTarget; 1773 | } 1774 | } 1775 | throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); 1776 | } 1777 | if (RegExpPrototypeExec( 1778 | invalidSegmentRegEx, 1779 | StringPrototypeSlice(target, 2) 1780 | ) !== null) 1781 | throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); 1782 | const resolved = new URL(target, packageJSONUrl); 1783 | const resolvedPath = resolved.pathname; 1784 | const packagePath = new URL(".", packageJSONUrl).pathname; 1785 | if (!StringPrototypeStartsWith(resolvedPath, packagePath)) 1786 | throwInvalidPackageTarget(match, target, packageJSONUrl, internal, base); 1787 | if (subpath === "") return resolved; 1788 | if (RegExpPrototypeExec(invalidSegmentRegEx, subpath) !== null) { 1789 | const request = pattern ? StringPrototypeReplace(match, "*", () => subpath) : match + subpath; 1790 | throwInvalidSubpath(request, packageJSONUrl, internal, base); 1791 | } 1792 | if (pattern) { 1793 | return new URL( 1794 | RegExpPrototypeSymbolReplace(patternRegEx, resolved.href, () => subpath) 1795 | ); 1796 | } 1797 | return new URL(subpath, resolved); 1798 | } 1799 | function isArrayIndex(key) { 1800 | const keyNum = +key; 1801 | if (`${keyNum}` !== key) return false; 1802 | return keyNum >= 0 && keyNum < 4294967295; 1803 | } 1804 | function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, base, pattern, internal, conditions) { 1805 | if (typeof target === "string") { 1806 | return resolvePackageTargetString( 1807 | target, 1808 | subpath, 1809 | packageSubpath, 1810 | packageJSONUrl, 1811 | base, 1812 | pattern, 1813 | internal); 1814 | } else if (ArrayIsArray(target)) { 1815 | if (target.length === 0) { 1816 | return null; 1817 | } 1818 | let lastException; 1819 | for (let i = 0; i < target.length; i++) { 1820 | const targetItem = target[i]; 1821 | let resolveResult; 1822 | try { 1823 | resolveResult = resolvePackageTarget( 1824 | packageJSONUrl, 1825 | targetItem, 1826 | subpath, 1827 | packageSubpath, 1828 | base, 1829 | pattern, 1830 | internal, 1831 | conditions 1832 | ); 1833 | } catch (e) { 1834 | lastException = e; 1835 | if (e.code === "ERR_INVALID_PACKAGE_TARGET") { 1836 | continue; 1837 | } 1838 | throw e; 1839 | } 1840 | if (resolveResult === void 0) { 1841 | continue; 1842 | } 1843 | if (resolveResult === null) { 1844 | lastException = null; 1845 | continue; 1846 | } 1847 | return resolveResult; 1848 | } 1849 | if (lastException === void 0 || lastException === null) 1850 | return lastException; 1851 | throw lastException; 1852 | } else if (typeof target === "object" && target !== null) { 1853 | const keys = ObjectGetOwnPropertyNames(target); 1854 | for (let i = 0; i < keys.length; i++) { 1855 | const key = keys[i]; 1856 | if (isArrayIndex(key)) { 1857 | throw new ERR_INVALID_PACKAGE_CONFIG( 1858 | fileURLToPath(packageJSONUrl), 1859 | base, 1860 | '"exports" cannot contain numeric property keys.' 1861 | ); 1862 | } 1863 | } 1864 | for (let i = 0; i < keys.length; i++) { 1865 | const key = keys[i]; 1866 | if (key === "default" || conditions.has(key)) { 1867 | const conditionalTarget = target[key]; 1868 | const resolveResult = resolvePackageTarget( 1869 | packageJSONUrl, 1870 | conditionalTarget, 1871 | subpath, 1872 | packageSubpath, 1873 | base, 1874 | pattern, 1875 | internal, 1876 | conditions 1877 | ); 1878 | if (resolveResult === void 0) continue; 1879 | return resolveResult; 1880 | } 1881 | } 1882 | return void 0; 1883 | } else if (target === null) { 1884 | return null; 1885 | } 1886 | throwInvalidPackageTarget( 1887 | packageSubpath, 1888 | target, 1889 | packageJSONUrl, 1890 | internal, 1891 | base 1892 | ); 1893 | } 1894 | function patternKeyCompare(a, b) { 1895 | const aPatternIndex = StringPrototypeIndexOf(a, "*"); 1896 | const bPatternIndex = StringPrototypeIndexOf(b, "*"); 1897 | const baseLenA = aPatternIndex === -1 ? a.length : aPatternIndex + 1; 1898 | const baseLenB = bPatternIndex === -1 ? b.length : bPatternIndex + 1; 1899 | if (baseLenA > baseLenB) return -1; 1900 | if (baseLenB > baseLenA) return 1; 1901 | if (aPatternIndex === -1) return 1; 1902 | if (bPatternIndex === -1) return -1; 1903 | if (a.length > b.length) return -1; 1904 | if (b.length > a.length) return 1; 1905 | return 0; 1906 | } 1907 | function packageImportsResolve({ name, base, conditions, readFileSyncFn }) { 1908 | if (name === "#" || StringPrototypeStartsWith(name, "#/") || StringPrototypeEndsWith(name, "/")) { 1909 | const reason = "is not a valid internal imports specifier name"; 1910 | throw new ERR_INVALID_MODULE_SPECIFIER(name, reason, fileURLToPath(base)); 1911 | } 1912 | let packageJSONUrl; 1913 | const packageConfig = getPackageScopeConfig(base, readFileSyncFn); 1914 | if (packageConfig.exists) { 1915 | packageJSONUrl = pathToFileURL(packageConfig.pjsonPath); 1916 | const imports = packageConfig.imports; 1917 | if (imports) { 1918 | if (ObjectPrototypeHasOwnProperty(imports, name) && !StringPrototypeIncludes(name, "*")) { 1919 | const resolveResult = resolvePackageTarget( 1920 | packageJSONUrl, 1921 | imports[name], 1922 | "", 1923 | name, 1924 | base, 1925 | false, 1926 | true, 1927 | conditions 1928 | ); 1929 | if (resolveResult != null) { 1930 | return resolveResult; 1931 | } 1932 | } else { 1933 | let bestMatch = ""; 1934 | let bestMatchSubpath; 1935 | const keys = ObjectGetOwnPropertyNames(imports); 1936 | for (let i = 0; i < keys.length; i++) { 1937 | const key = keys[i]; 1938 | const patternIndex = StringPrototypeIndexOf(key, "*"); 1939 | if (patternIndex !== -1 && StringPrototypeStartsWith( 1940 | name, 1941 | StringPrototypeSlice(key, 0, patternIndex) 1942 | )) { 1943 | const patternTrailer = StringPrototypeSlice(key, patternIndex + 1); 1944 | if (name.length >= key.length && StringPrototypeEndsWith(name, patternTrailer) && patternKeyCompare(bestMatch, key) === 1 && StringPrototypeLastIndexOf(key, "*") === patternIndex) { 1945 | bestMatch = key; 1946 | bestMatchSubpath = StringPrototypeSlice( 1947 | name, 1948 | patternIndex, 1949 | name.length - patternTrailer.length 1950 | ); 1951 | } 1952 | } 1953 | } 1954 | if (bestMatch) { 1955 | const target = imports[bestMatch]; 1956 | const resolveResult = resolvePackageTarget( 1957 | packageJSONUrl, 1958 | target, 1959 | bestMatchSubpath, 1960 | bestMatch, 1961 | base, 1962 | true, 1963 | true, 1964 | conditions 1965 | ); 1966 | if (resolveResult != null) { 1967 | return resolveResult; 1968 | } 1969 | } 1970 | } 1971 | } 1972 | } 1973 | throwImportNotDefined(name, packageJSONUrl, base); 1974 | } 1975 | 1976 | let findPnpApi = esmModule.findPnpApi; 1977 | if (!findPnpApi) { 1978 | const require = createRequire(import.meta.url); 1979 | const pnpApi = require(`./.pnp.cjs`); 1980 | pnpApi.setup(); 1981 | findPnpApi = esmModule.findPnpApi; 1982 | } 1983 | const pathRegExp = /^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:node:)?(?:@[^/]+\/)?[^/]+)\/*(.*|)$/; 1984 | const isRelativeRegexp = /^\.{0,2}\//; 1985 | function tryReadFile(filePath) { 1986 | try { 1987 | return fs.readFileSync(filePath, `utf8`); 1988 | } catch (err) { 1989 | if (err.code === `ENOENT`) 1990 | return void 0; 1991 | throw err; 1992 | } 1993 | } 1994 | async function resolvePrivateRequest(specifier, issuer, context, nextResolve) { 1995 | const resolved = packageImportsResolve({ 1996 | name: specifier, 1997 | base: pathToFileURL(issuer), 1998 | conditions: new Set(context.conditions), 1999 | readFileSyncFn: tryReadFile 2000 | }); 2001 | if (resolved instanceof URL) { 2002 | return { url: resolved.href, shortCircuit: true }; 2003 | } else { 2004 | if (resolved.startsWith(`#`)) 2005 | throw new Error(`Mapping from one private import to another isn't allowed`); 2006 | return resolve$1(resolved, context, nextResolve); 2007 | } 2008 | } 2009 | async function resolve$1(originalSpecifier, context, nextResolve) { 2010 | if (!findPnpApi || isBuiltin(originalSpecifier)) 2011 | return nextResolve(originalSpecifier, context, nextResolve); 2012 | let specifier = originalSpecifier; 2013 | const url = tryParseURL(specifier, isRelativeRegexp.test(specifier) ? context.parentURL : void 0); 2014 | if (url) { 2015 | if (url.protocol !== `file:`) 2016 | return nextResolve(originalSpecifier, context, nextResolve); 2017 | specifier = fileURLToPath(url); 2018 | } 2019 | const { parentURL, conditions = [] } = context; 2020 | const issuer = parentURL && tryParseURL(parentURL)?.protocol === `file:` ? fileURLToPath(parentURL) : process.cwd(); 2021 | const pnpapi = findPnpApi(issuer) ?? (url ? findPnpApi(specifier) : null); 2022 | if (!pnpapi) 2023 | return nextResolve(originalSpecifier, context, nextResolve); 2024 | if (specifier.startsWith(`#`)) 2025 | return resolvePrivateRequest(specifier, issuer, context, nextResolve); 2026 | const dependencyNameMatch = specifier.match(pathRegExp); 2027 | let allowLegacyResolve = false; 2028 | if (dependencyNameMatch) { 2029 | const [, dependencyName, subPath] = dependencyNameMatch; 2030 | if (subPath === `` && dependencyName !== `pnpapi`) { 2031 | const resolved = pnpapi.resolveToUnqualified(`${dependencyName}/package.json`, issuer); 2032 | if (resolved) { 2033 | const content = await tryReadFile$1(resolved); 2034 | if (content) { 2035 | const pkg = JSON.parse(content); 2036 | allowLegacyResolve = pkg.exports == null; 2037 | } 2038 | } 2039 | } 2040 | } 2041 | let result; 2042 | try { 2043 | result = pnpapi.resolveRequest(specifier, issuer, { 2044 | conditions: new Set(conditions), 2045 | // TODO: Handle --experimental-specifier-resolution=node 2046 | extensions: allowLegacyResolve ? void 0 : [] 2047 | }); 2048 | } catch (err) { 2049 | if (err instanceof Error && `code` in err && err.code === `MODULE_NOT_FOUND`) 2050 | err.code = `ERR_MODULE_NOT_FOUND`; 2051 | throw err; 2052 | } 2053 | if (!result) 2054 | throw new Error(`Resolving '${specifier}' from '${issuer}' failed`); 2055 | const resultURL = pathToFileURL(result); 2056 | if (url) { 2057 | resultURL.search = url.search; 2058 | resultURL.hash = url.hash; 2059 | } 2060 | if (!parentURL) 2061 | setEntrypointPath(fileURLToPath(resultURL)); 2062 | return { 2063 | url: resultURL.href, 2064 | shortCircuit: true 2065 | }; 2066 | } 2067 | 2068 | if (!HAS_LAZY_LOADED_TRANSLATORS) { 2069 | const binding = process.binding(`fs`); 2070 | const originalReadFile = binding.readFileUtf8 || binding.readFileSync; 2071 | if (originalReadFile) { 2072 | binding[originalReadFile.name] = function(...args) { 2073 | try { 2074 | return fs.readFileSync(args[0], { 2075 | encoding: `utf8`, 2076 | // @ts-expect-error - The docs says it needs to be a string but 2077 | // links to https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#file-system-flags 2078 | // which says it can be a number which matches the implementation. 2079 | flag: args[1] 2080 | }); 2081 | } catch { 2082 | } 2083 | return originalReadFile.apply(this, args); 2084 | }; 2085 | } else { 2086 | const binding2 = process.binding(`fs`); 2087 | const originalfstat = binding2.fstat; 2088 | const ZIP_MASK = 4278190080; 2089 | const ZIP_MAGIC = 704643072; 2090 | binding2.fstat = function(...args) { 2091 | const [fd, useBigint, req] = args; 2092 | if ((fd & ZIP_MASK) === ZIP_MAGIC && useBigint === false && req === void 0) { 2093 | try { 2094 | const stats = fs.fstatSync(fd); 2095 | return new Float64Array([ 2096 | stats.dev, 2097 | stats.mode, 2098 | stats.nlink, 2099 | stats.uid, 2100 | stats.gid, 2101 | stats.rdev, 2102 | stats.blksize, 2103 | stats.ino, 2104 | stats.size, 2105 | stats.blocks 2106 | // atime sec 2107 | // atime ns 2108 | // mtime sec 2109 | // mtime ns 2110 | // ctime sec 2111 | // ctime ns 2112 | // birthtime sec 2113 | // birthtime ns 2114 | ]); 2115 | } catch { 2116 | } 2117 | } 2118 | return originalfstat.apply(this, args); 2119 | }; 2120 | } 2121 | } 2122 | 2123 | const resolve = resolve$1; 2124 | const load = load$1; 2125 | 2126 | export { load, resolve }; 2127 | -------------------------------------------------------------------------------- /.yarn/install-state.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeong-sik/kakao-api-mcp-server/bf8bbdb2d6f882dbbba20082a5b9e93fefd39ed0/.yarn/install-state.gz -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jeong-Sik Yun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 카카오 API MCP 서버 2 | 3 | 카카오맵 API 및 Daum 검색 API를 [Model Context Protocol](https://github.com/anthropics/model-context-protocol)(MCP)을 통해 활용할 수 있는 서버입니다. 이 서버를 통해 AI 모델이 카카오맵의 지도 관련 기능과 Daum의 다양한 검색 기능을 활용할 수 있습니다. 4 | 5 | ## 주의 사항 6 | 7 | * **카카오 로그인, 카카오톡 메시지 보내기 등 사용자 계정 관련 기능은 포함되어 있지 않습니다.** 이 서버는 공개된 카카오 및 Daum의 Open API만을 사용합니다. 8 | * 본 서버를 사용하기 위해서는 유효한 **카카오 REST API 키**가 필요합니다. [카카오 디벨로퍼스](https://developers.kakao.com/)에서 앱을 생성하고 REST API 키를 발급받으세요. 9 | 10 | ## 주요 기능 11 | 12 | ### 카카오맵 API 13 | 14 | 1. **장소 검색 (`mcp_kakao_map_search_places`)** 15 | - 키워드로 카카오맵에서 장소를 검색 16 | - 위치, 카테고리, 연락처 정보 제공 17 | 18 | 2. **좌표-주소 변환 (`mcp_kakao_map_coord_to_address`)** 19 | - 경위도 좌표를 실제 주소로 변환 20 | - 도로명 주소 및 지번 주소 정보 제공 21 | 22 | 3. **길찾기 (`mcp_kakao_map_find_route`)** 23 | - 출발지에서 목적지까지의 경로 검색 24 | - 거리, 소요 시간, 예상 택시 요금 등 제공 25 | - 교통 상황 정보 포함 (선택적) 26 | 27 | ### Daum 검색 API 28 | 29 | 1. **웹 문서 검색 (`mcp_kakao_map_search_web`)** 30 | - 키워드로 다음 웹 문서 검색 31 | - 페이지 정렬 및 검색 결과 개수 조정 가능 32 | 33 | 2. **이미지 검색 (`mcp_kakao_map_search_image`)** 34 | - 키워드로 다음 이미지 검색 35 | - 이미지 URL 및 관련 정보 제공 36 | 37 | 3. **블로그 검색 (`mcp_kakao_map_search_blog`)** 38 | - 키워드로 다음 블로그 글 검색 39 | - 블로그 이름, 포스트 제목, 내용 요약 제공 40 | 41 | 4. **카페 검색 (`mcp_kakao_map_search_cafe`)** 42 | - 키워드로 다음 카페 글 검색 43 | - 카페 이름, 게시물 제목, 내용 요약 제공 44 | 45 | ## 도구 사용 예시 (MCP) 46 | 47 | 아래는 MCP 클라이언트(예: AI 모델)가 이 서버의 도구를 호출하는 방법과 예상되는 응답 형식입니다. 48 | 49 | ### 카카오맵 API 50 | 51 | #### 1. 장소 검색 (`mcp_kakao_map_search_places`) 52 | 53 | **호출 (Request):** 54 | ```json 55 | { 56 | "tool_name": "mcp_kakao_map_search_places", 57 | "parameters": { 58 | "keyword": "판교역 현대백화점" 59 | } 60 | } 61 | ``` 62 | 63 | **응답 (Response - 예시):** 64 | ```json 65 | { 66 | "tool_name": "mcp_kakao_map_search_places", 67 | "result": "장소 검색 결과 (결과 수: 15, 총 18개):\n이름: 현대백화점 판교점\n주소: 경기 성남시 분당구 백현동 541\n카테고리: 쇼핑,유통 > 백화점 > 현대백화점\n전화번호: 031-5170-2233\n상세정보: http://place.map.kakao.com/18757447\n---\n... (추가 결과)" 68 | } 69 | ``` 70 | 71 | #### 2. 좌표-주소 변환 (`mcp_kakao_map_coord_to_address`) 72 | 73 | **호출 (Request):** 74 | ```json 75 | { 76 | "tool_name": "mcp_kakao_map_coord_to_address", 77 | "parameters": { 78 | "x": 127.1120278, 79 | "y": 37.3955833 80 | } 81 | } 82 | ``` 83 | 84 | **응답 (Response - 예시):** 85 | ```json 86 | { 87 | "tool_name": "mcp_kakao_map_coord_to_address", 88 | "result": "주소 변환 결과:\n도로명: 경기 성남시 분당구 판교역로146번길 20\n지번: 경기 성남시 분당구 백현동 535" 89 | } 90 | ``` 91 | 92 | #### 3. 길찾기 (`mcp_kakao_map_find_route`) 93 | 94 | **호출 (Request):** 95 | ```json 96 | { 97 | "tool_name": "mcp_kakao_map_find_route", 98 | "parameters": { 99 | "origin": "판교역", 100 | "destination": "정자역", 101 | "transportation_type": "car", 102 | "traffic_info": true 103 | } 104 | } 105 | ``` 106 | 107 | **응답 (Response - 예시):** 108 | ```json 109 | { 110 | "tool_name": "mcp_kakao_map_find_route", 111 | "result": "🗺️ 길찾기 결과\n\n출발지: 판교역 신분당선 (경기 성남시 분당구 삼평동)\n\n목적지: 정자역 신분당선 (경기 성남시 분당구 정자동)\n\n이동 수단: 자동차\n\n총 거리: 3.6km\n예상 소요 시간: 10분\n예상 택시 요금: 5,600원\n\n📊 교통 상황 요약:\n\n카카오맵에서 보기: https://map.kakao.com/?sName=%ED%8C%90%EA%B5%90%EC%97%AD&eName=%EC%A0%95%EC%9E%90%EC%97%AD\n" 112 | } 113 | ``` 114 | 115 | ### Daum 검색 API 116 | 117 | #### 1. 웹 문서 검색 (`mcp_kakao_map_search_web`) 118 | 119 | **호출 (Request):** 120 | ```json 121 | { 122 | "tool_name": "mcp_kakao_map_search_web", 123 | "parameters": { 124 | "query": "카카오브레인 칼로", 125 | "size": 2 126 | } 127 | } 128 | ``` 129 | 130 | **응답 (Response - 예시):** 131 | ```json 132 | { 133 | "tool_name": "mcp_kakao_map_search_web", 134 | "result": "웹 문서 검색 결과 (총 2083개 중 2개 표시):\n\n1. 카카오브레인 | 칼로 Karlo\n 내용: 카카오브레인의 이미지 생성 모델 Karlo는 사용자가 입력한 문장(Text)을 이해하여, 세상에 단 하나뿐인 이미지를 만들어내는 인공지능 화가입니다. 수백만 장 규모의...\n URL: https://kakaobrain.com/karlo\n 날짜: 2024. 1. 1.\n\n2. 카카오브레인, AI 아티스트 '칼로 2.0' 공개 - 테크레시피\n 내용: 카카오브레인이 초거대 인공지능(AI) 이미지 생성 모델 '칼로(Karlo) 2.0'을 공개했다고 11일 밝혔다. 칼로 2.0은 약 3억 장 규모의 텍스트-이미지 데이터셋을 학습한 모델이...\n URL: https://techrecipe.co.kr/posts/56513\n 날짜: 2023. 7. 11.\n\n현재 페이지가 마지막 페이지가 아닙니다. 더 많은 결과를 보려면 page 매개변수를 증가시키세요.\n" 135 | } 136 | ``` 137 | 138 | #### 2. 이미지 검색 (`mcp_kakao_map_search_image`) 139 | 140 | **호출 (Request):** 141 | ```json 142 | { 143 | "tool_name": "mcp_kakao_map_search_image", 144 | "parameters": { 145 | "query": "고양이", 146 | "size": 1 147 | } 148 | } 149 | ``` 150 | 151 | **응답 (Response - 예시):** 152 | ```json 153 | { 154 | "tool_name": "mcp_kakao_map_search_image", 155 | "result": "이미지 검색 결과 (총 8715385개 중 1개 표시):\n\n1. 컬렉션 이름: Daum 백과\n 문서 URL: http://100.daum.net/encyclopedia/view/172XX61300001\n 이미지 URL: https://t1.daumcdn.net/thumb/R1024x0/?fname=http%3A%2F%2Ft1.daumcdn.net%2Fencyclop%2F172%2F613%2F172XX61300001\n 썸네일 URL: https://search1.kakaocdn.net/thumb/R100x100/?fname=http%3A%2F%2Ft1.daumcdn.net%2Fencyclop%2F172%2F613%2F172XX61300001&token=1579057346066cfd0b2e0c671d07c433\n 크기: 가로 1024px, 세로 682px\n 표시 URL: 100.daum.net\n 날짜: 2014. 11. 6.\n\n현재 페이지가 마지막 페이지가 아닙니다. 더 많은 결과를 보려면 page 매개변수를 증가시키세요.\n" 156 | } 157 | ``` 158 | 159 | #### 3. 블로그 검색 (`mcp_kakao_map_search_blog`) 160 | 161 | **호출 (Request):** 162 | ```json 163 | { 164 | "tool_name": "mcp_kakao_map_search_blog", 165 | "parameters": { 166 | "query": "판교 맛집", 167 | "size": 1 168 | } 169 | } 170 | ``` 171 | 172 | **응답 (Response - 예시):** 173 | ```json 174 | { 175 | "tool_name": "mcp_kakao_map_search_blog", 176 | "result": "블로그 검색 결과 (총 215893개 중 1개 표시):\n\n1. 블로그명: 짱돌의 일상다반사\n 제목: 판교 맛집 추천 | 유스페이스몰 가성비 좋은 점심 맛집\n 내용: 판교테크노밸리 유스페이스몰은 늘 점심시간마다 직장인들로 인산인해를 이루는 곳이다. 오늘은 판교 점심 맛집으로 괜찮은 곳 두 군데를 소개해 본다. 1.... \n URL: http://jdcamping.tistory.com/1374\n 썸네일: https://search2.kakaocdn.net/thumb/R180x180/?fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQv0tX%2FbtrOfR4oUu3%2FdKQGkK0kY6kKk40f4kYkYK%2Fimg.jpg&token=1c251bb24ae4bb01657303012e2641ac\n 날짜: 2024. 12. 17.\n\n현재 페이지가 마지막 페이지가 아닙니다. 더 많은 결과를 보려면 page 매개변수를 증가시키세요.\n" 177 | } 178 | ``` 179 | 180 | #### 4. 카페 검색 (`mcp_kakao_map_search_cafe`) 181 | 182 | **호출 (Request):** 183 | ```json 184 | { 185 | "tool_name": "mcp_kakao_map_search_cafe", 186 | "parameters": { 187 | "query": "코딩 스터디", 188 | "size": 1 189 | } 190 | } 191 | ``` 192 | 193 | **응답 (Response - 예시):** 194 | ```json 195 | { 196 | "tool_name": "mcp_kakao_map_search_cafe", 197 | "result": "카페 검색 결과 (총 18335개 중 1개 표시):\n\n1. 카페명: 독취사-취업,대학생,대기업,공기업,NCS,인적성,취업카페\n 제목: [스터디] 웹개발/코딩 기초 스터디 구해요\n 내용: 안녕하세요! 웹개발 및 코딩 기초를 함께 공부할 스터디원을 모집합니다. 현재 2명이며, 최대 4명까지 생각하고 있습니다. 장소는 주로 강남/사당에서 진행하고, 온라...\n URL: http://cafe.daum.net/breakjob/DldL/12345\n 썸네일: https://search1.kakaocdn.net/thumb/P180x180/?fname=https%3A%2F%2Ft1.daumcdn.net%2Fcafe_image%2F%2Fconfig%2Fimg_default_profile%3Fver%3D1&token=de43b9d06222d0a2192f9f70fcb0f134\n 날짜: 2025. 3. 28.\n\n현재 페이지가 마지막 페이지가 아닙니다. 더 많은 결과를 보려면 page 매개변수를 증가시키세요.\n" 198 | } 199 | ``` 200 | 201 | ## 설치 및 설정 202 | 203 | 1. **저장소 복제 및 종속성 설치:** 204 | ```bash 205 | git clone https://github.com/yousleepwhen/kakao-api-mcp-server.git # 저장소 URL을 실제 URL로 변경해주세요 206 | cd kakao-api-mcp-server 207 | yarn install 208 | ``` 209 | * 이 프로젝트는 `yarn` 패키지 매니저 사용을 권장합니다. 210 | 211 | 2. **카카오 REST API 키 설정:** 212 | - 프로젝트 루트 디렉토리에 `.env` 파일을 생성합니다. 213 | - `.env` 파일 안에 다음과 같이 카카오 디벨로퍼스에서 발급받은 REST API 키를 입력합니다: 214 | ``` 215 | KAKAO_REST_API_KEY=여기에_카카오_REST_API_키_입력 216 | ``` 217 | - 또는, 서버 실행 시 `--kakao-api-key` 인자를 통해 직접 전달할 수도 있습니다. 218 | 219 | ## 실행 방법 220 | 221 | 서버를 실행하기 전에 코드를 빌드해야 합니다. `start` 관련 스크립트에 빌드 과정이 포함되어 있으므로 별도로 `yarn build`를 실행할 필요는 없습니다. 222 | 223 | ### HTTP 모드 (기본) 224 | 225 | 다른 서비스나 도구와 HTTP를 통해 통신할 때 사용합니다. 226 | 227 | ```bash 228 | yarn start 229 | ``` 230 | 231 | 기본적으로 3000번 포트를 사용합니다. 포트를 변경하려면 `--port` 인자를 사용하세요: 232 | 233 | ```bash 234 | yarn start --port 8080 235 | ``` 236 | 237 | ### stdio 모드 238 | 239 | 터미널의 표준 입출력(stdin/stdout)을 통해 MCP 메시지를 주고받을 때 사용합니다. 240 | 241 | ```bash 242 | yarn start:stdio 243 | ``` 244 | 245 | ### 개발 모드 246 | 247 | 개발 중 코드 변경 시 자동으로 빌드하고 서버를 재시작하려면 (nodemon 등 별도 설정 필요) `dev` 스크립트를 활용할 수 있습니다. 현재 `dev` 스크립트는 `start`와 동일하게 동작합니다. 248 | 249 | ```bash 250 | yarn dev 251 | ``` 252 | 253 | ## 라이선스 254 | 255 | 이 프로젝트는 MIT 라이선스 하에 배포됩니다. -------------------------------------------------------------------------------- /mcp.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "servers": [ 3 | { 4 | "name": "kakao-map", 5 | "command": ["node", "dist/index.js", "--mode=stdio", "--kakao-api-key=your_kakao_api_key_here"] 6 | } 7 | ] 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kakao-api-mcp-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsc -p tsconfig.json", 8 | "start": "yarn build && node dist/index.js --mode=http --port=3000", 9 | "start:stdio": "yarn build && node dist/index.js --mode=stdio", 10 | "dev": "yarn build && node dist/index.js --mode=http --port=3000" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "description": "", 16 | "dependencies": { 17 | "@modelcontextprotocol/sdk": "^1.8.0", 18 | "cors": "^2.8.5", 19 | "express": "^5.1.0" 20 | }, 21 | "devDependencies": { 22 | "@types/cors": "^2.8.17", 23 | "@types/express": "^5.0.1", 24 | "axios": "^1.8.4", 25 | "eventsource": "^3.0.6", 26 | "node-fetch": "^3.3.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { z } from "zod"; 3 | import axios, { type AxiosError } from "axios"; 4 | import dotenv from "dotenv"; 5 | import yargs from "yargs"; 6 | import { hideBin } from "yargs/helpers"; 7 | import express, { type Request, type Response } from "express"; 8 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 9 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 10 | import type { IncomingMessage, ServerResponse } from "node:http"; 11 | import cors from "cors"; 12 | import { fileURLToPath } from "url"; 13 | 14 | // Load environment variables from .env file 15 | dotenv.config(); 16 | 17 | // --- Configuration --- 18 | 19 | const argv = yargs(hideBin(process.argv)) 20 | .option('kakao-api-key', { 21 | alias: 'k', 22 | type: 'string', 23 | description: 'Kakao REST API Key', 24 | }) 25 | .option('mode', { 26 | type: 'string', 27 | choices: ['stdio', 'http'], 28 | default: 'stdio', 29 | description: 'Transport mode: stdio or http (default: stdio)', 30 | }) 31 | .option('port', { 32 | type: 'number', 33 | default: 3000, 34 | description: 'Port for HTTP server (HTTP mode only)', 35 | }) 36 | .help() 37 | .alias('help', 'h') 38 | .parseSync(); 39 | 40 | // Custom logger to prevent interference with stdio transport 41 | const logger = { 42 | log: (message: string, ...args: unknown[]) => { 43 | // In stdio mode, write to stderr to avoid interfering with JSON messages 44 | if (argv.mode === 'stdio') { 45 | process.stderr.write(`LOG: ${message}\n`); 46 | } else { 47 | console.log(message, ...args); 48 | } 49 | }, 50 | error: (message: string, ...args: unknown[]) => { 51 | // Always write errors to stderr 52 | if (argv.mode === 'stdio') { 53 | process.stderr.write(`ERROR: ${message}\n`); 54 | } else { 55 | console.error(message, ...args); 56 | } 57 | } 58 | }; 59 | 60 | // Get Kakao API Key: prioritize command-line arg, then env var 61 | const KAKAO_API_KEY = argv.kakaoApiKey || process.env.KAKAO_REST_API_KEY; 62 | 63 | if (!KAKAO_API_KEY) { 64 | logger.error( 65 | "Error: Kakao REST API Key not found. " + 66 | "Provide it via --kakao-api-key argument or KAKAO_REST_API_KEY environment variable." 67 | ); 68 | process.exit(1); // Exit if no key is found 69 | } 70 | 71 | logger.log("Kakao REST API Key loaded successfully."); 72 | 73 | // --- Define Kakao API Response Types --- 74 | 75 | interface KakaoPlaceDocument { 76 | place_name: string; 77 | address_name: string; 78 | category_name: string; 79 | place_url: string; 80 | phone?: string; 81 | x?: string; 82 | y?: string; 83 | } 84 | 85 | interface KakaoKeywordSearchMeta { 86 | total_count: number; 87 | pageable_count: number; 88 | is_end: boolean; 89 | } 90 | 91 | interface KakaoKeywordSearchResponse { 92 | documents: KakaoPlaceDocument[]; 93 | meta: KakaoKeywordSearchMeta; 94 | } 95 | 96 | interface KakaoAddress { 97 | address_name: string; 98 | region_1depth_name: string; 99 | region_2depth_name: string; 100 | region_3depth_name: string; 101 | mountain_yn: string; 102 | main_address_no: string; 103 | sub_address_no?: string; 104 | zip_code?: string; 105 | } 106 | 107 | interface KakaoRoadAddress { 108 | address_name: string; 109 | region_1depth_name: string; 110 | region_2depth_name: string; 111 | region_3depth_name: string; 112 | road_name: string; 113 | underground_yn: string; 114 | main_building_no: string; 115 | sub_building_no?: string; 116 | building_name?: string; 117 | zone_no: string; 118 | } 119 | 120 | interface KakaoCoord2AddressDocument { 121 | road_address: KakaoRoadAddress | null; 122 | address: KakaoAddress | null; 123 | } 124 | 125 | interface KakaoCoord2AddressResponse { 126 | meta: { total_count: number }; 127 | documents: KakaoCoord2AddressDocument[]; 128 | } 129 | 130 | // Daum 검색 API 응답 타입 정의 131 | interface DaumSearchResponse { 132 | meta: { 133 | total_count: number; 134 | pageable_count: number; 135 | is_end: boolean; 136 | }; 137 | documents: Record[]; 138 | } 139 | 140 | // <<< WaypointResult 인터페이스 정의 이동 >>> 141 | interface WaypointResult { 142 | success: boolean; 143 | name: string; 144 | placeName?: string; 145 | addressName?: string; 146 | x?: string; 147 | y?: string; 148 | } 149 | 150 | // --- MCP Server Setup --- 151 | 152 | const server = new McpServer( 153 | { 154 | name: "kakao-map", 155 | version: "0.1.0" 156 | }, 157 | { 158 | capabilities: { 159 | logging: {}, 160 | tools: {} 161 | } 162 | } 163 | ); 164 | 165 | // 위 코드 대신 MCP SDK의 문서나 타입을 확인하여 올바른 이벤트 처리 방식 적용 166 | // 임시로 주석 처리 167 | 168 | // 카카오맵 API용 axios 인스턴스 생성 169 | const kakaoApiClient = axios.create({ 170 | baseURL: 'https://dapi.kakao.com', 171 | headers: { 172 | Authorization: `KakaoAK ${KAKAO_API_KEY}`, 173 | } 174 | }); 175 | 176 | // 카카오모빌리티 API용 axios 인스턴스 생성 177 | const kakaoMobilityApiClient = axios.create({ 178 | baseURL: 'https://apis-navi.kakaomobility.com', 179 | headers: { 180 | Authorization: `KakaoAK ${KAKAO_API_KEY}`, 181 | } 182 | }); 183 | 184 | // axios 인스턴스에 인터셉터 추가 185 | kakaoApiClient.interceptors.request.use(request => { 186 | logger.log(`Kakao API Request: ${request.method?.toUpperCase()} ${request.url}`); 187 | return request; 188 | }); 189 | 190 | kakaoApiClient.interceptors.response.use(response => { 191 | logger.log(`Kakao API Response: ${response.status} ${response.statusText}`); 192 | return response; 193 | }, error => { 194 | logger.error(`Kakao API Error: ${error.message}`); 195 | return Promise.reject(error); 196 | }); 197 | 198 | kakaoMobilityApiClient.interceptors.request.use(request => { 199 | logger.log(`Kakao Mobility API Request: ${request.method?.toUpperCase()} ${request.url}`); 200 | logger.log(`Request Headers: ${JSON.stringify(request.headers)}`); 201 | logger.log(`Request Params: ${JSON.stringify(request.params)}`); 202 | return request; 203 | }); 204 | 205 | kakaoMobilityApiClient.interceptors.response.use(response => { 206 | logger.log(`Kakao Mobility API Response: ${response.status} ${response.statusText}`); 207 | return response; 208 | }, error => { 209 | logger.error(`Kakao Mobility API Error: ${error.message}`); 210 | if (axios.isAxiosError(error)) { 211 | logger.error(`Status: ${error.response?.status}`); 212 | logger.error(`Data: ${JSON.stringify(error.response?.data)}`); 213 | } 214 | return Promise.reject(error); 215 | }); 216 | 217 | // Tool: search-places 218 | const searchPlacesSchema = z.object({ 219 | keyword: z.string().describe('검색할 키워드 (예: "강남역 맛집")'), 220 | x: z.number().optional().describe("중심 좌표의 X 또는 longitude 값 (WGS84)"), 221 | y: z.number().optional().describe("중심 좌표의 Y 또는 latitude 값 (WGS84)"), 222 | radius: z.number().int().min(0).max(20000).optional().describe("중심 좌표부터의 검색 반경(0~20000m)"), 223 | }); 224 | server.tool( 225 | "search-places", 226 | "키워드를 사용하여 카카오맵에서 장소를 검색합니다.", 227 | searchPlacesSchema.shape, 228 | async (params: z.infer) => { 229 | logger.log("Executing search-places tool with params:", params); 230 | try { 231 | const response = await kakaoApiClient.get( 232 | "/v2/local/search/keyword.json", 233 | { 234 | params: { 235 | query: params.keyword, 236 | x: params.x, 237 | y: params.y, 238 | radius: params.radius, 239 | }, 240 | } 241 | ); 242 | 243 | logger.log(`API Response received: ${response.status}`); 244 | logger.log(`Results count: ${response.data.documents?.length || 0}`); 245 | 246 | const formattedResponse = formatPlacesResponse(response.data); 247 | 248 | logger.log(`Formatted response: ${formattedResponse.substring(0, 100)}...`); 249 | 250 | // 결과를 STDOUT에 명시적으로 출력 (디버깅용) 251 | if (argv.mode === 'stdio') { 252 | const result = { 253 | type: "tool_response", 254 | tool: "search-places", 255 | content: [{ type: "text", text: formattedResponse }] 256 | }; 257 | console.log(JSON.stringify(result)); 258 | } 259 | 260 | return { 261 | content: [{ type: "text", text: formattedResponse }], 262 | }; 263 | } catch (error: unknown) { 264 | let errorMessage = "알 수 없는 오류 발생"; 265 | if (axios.isAxiosError(error)) { 266 | const axiosError = error as AxiosError<{ message?: string }>; 267 | logger.error("API Error status:", axiosError.response?.status); 268 | logger.error("API Error details:", JSON.stringify(axiosError.response?.data)); 269 | errorMessage = axiosError.response?.data?.message || axiosError.message; 270 | } else if (error instanceof Error) { 271 | errorMessage = error.message; 272 | } 273 | 274 | // 오류 결과를 STDOUT에 명시적으로 출력 (디버깅용) 275 | if (argv.mode === 'stdio') { 276 | const errorResult = { 277 | type: "tool_response", 278 | tool: "search-places", 279 | content: [{ type: "text", text: `장소 검색 중 오류 발생: ${errorMessage}` }] 280 | }; 281 | console.log(JSON.stringify(errorResult)); 282 | } 283 | 284 | return { 285 | content: [{ type: "text", text: `장소 검색 중 오류 발생: ${errorMessage}` }], 286 | }; 287 | } 288 | } 289 | ); 290 | 291 | // Tool: coord-to-address 292 | const coordToAddressSchema = z.object({ 293 | x: z.number().describe("경도 (longitude) WGS84 좌표"), 294 | y: z.number().describe("위도 (latitude) WGS84 좌표"), 295 | }); 296 | server.tool( 297 | "coord-to-address", 298 | "좌표(경도, 위도)를 주소(도로명, 지번)로 변환합니다.", 299 | coordToAddressSchema.shape, 300 | async (params: z.infer) => { 301 | logger.log("Executing coord-to-address tool with params:", params); 302 | try { 303 | const response = await kakaoApiClient.get( 304 | "https://dapi.kakao.com/v2/local/geo/coord2address.json", 305 | { 306 | params: { 307 | x: params.x, 308 | y: params.y, 309 | }, 310 | } 311 | ); 312 | const formattedResponse = formatAddressResponse(response.data); 313 | return { 314 | content: [{ type: "text", text: formattedResponse }], 315 | }; 316 | } catch (error: unknown) { 317 | let errorMessage = "알 수 없는 오류 발생"; 318 | if (axios.isAxiosError(error)) { 319 | const axiosError = error as AxiosError<{ message?: string }>; 320 | logger.error("API Error status:", axiosError.response?.status); 321 | errorMessage = axiosError.response?.data?.message || axiosError.message; 322 | } else if (error instanceof Error) { 323 | errorMessage = error.message; 324 | } 325 | return { 326 | content: [{ type: "text", text: `좌표-주소 변환 중 오류 발생: ${errorMessage}` }], 327 | }; 328 | } 329 | } 330 | ); 331 | 332 | // Tool: find-route 333 | const findRouteSchema = z.object({ 334 | origin: z.string().describe('출발지 이름 (예: "강남역")'), 335 | destination: z.string().describe('목적지 이름 (예: "코엑스")'), 336 | waypoints: z.array(z.string()).optional().describe('경유지 이름 목록 (선택사항)'), 337 | transportation_type: z.enum(["car", "public", "walk"]).default("car").describe("이동 수단 (자동차, 대중교통, 도보)"), 338 | priority: z.enum(["RECOMMEND", "TIME", "DISTANCE"]).default("RECOMMEND").describe("경로 탐색 우선순위 (추천, 최단시간, 최단거리)"), 339 | traffic_info: z.boolean().default(true).describe("교통 정보 포함 여부") 340 | }); 341 | 342 | server.tool( 343 | "find-route", 344 | "출발지에서 목적지까지의 길찾기 정보를 제공합니다.", 345 | findRouteSchema.shape, 346 | async (params: z.infer) => { 347 | logger.log("Executing find-route tool with params:", params); 348 | try { 349 | // 1. 출발지 검색 350 | const originResponse = await kakaoApiClient.get( 351 | "https://dapi.kakao.com/v2/local/search/keyword.json", 352 | { params: { query: params.origin } } 353 | ); 354 | // <<< 출발지 응답 로깅 추가 >>> 355 | logger.log("Origin Search Response:", JSON.stringify(originResponse.data, null, 2)); 356 | 357 | if (!originResponse.data.documents || originResponse.data.documents.length === 0) { 358 | return { 359 | content: [{ type: "text", text: `출발지 "${params.origin}"를 찾을 수 없습니다.` }] 360 | }; 361 | } 362 | 363 | // 2. 목적지 검색 364 | const destinationResponse = await kakaoApiClient.get( 365 | "https://dapi.kakao.com/v2/local/search/keyword.json", 366 | { params: { query: params.destination } } 367 | ); 368 | // <<< 목적지 응답 로깅 추가 >>> 369 | logger.log("Destination Search Response:", JSON.stringify(destinationResponse.data, null, 2)); 370 | 371 | if (!destinationResponse.data.documents || destinationResponse.data.documents.length === 0) { 372 | return { 373 | content: [{ type: "text", text: `목적지 "${params.destination}"를 찾을 수 없습니다.` }] 374 | }; 375 | } 376 | 377 | // 3. 경유지 검색 (있는 경우) 378 | const waypointsPromises = params.waypoints?.map(waypoint => 379 | kakaoApiClient.get( 380 | "https://dapi.kakao.com/v2/local/search/keyword.json", 381 | { params: { query: waypoint } } 382 | ) 383 | ) || []; 384 | 385 | const waypointsResponses = await Promise.all(waypointsPromises); 386 | const waypointsResults: WaypointResult[] = waypointsResponses.map((response, index) => { 387 | if (!response.data.documents || response.data.documents.length === 0) { 388 | return { success: false, name: params.waypoints?.[index] || "알 수 없음" }; 389 | } 390 | const place = response.data.documents[0]; 391 | return { 392 | success: true, 393 | name: params.waypoints?.[index] || "알 수 없음", 394 | placeName: place.place_name, 395 | addressName: place.address_name, 396 | x: place.x, 397 | y: place.y 398 | }; 399 | }); 400 | 401 | // 실패한 경유지가 있는지 확인 402 | const failedWaypoints = waypointsResults.filter(wp => !wp.success); 403 | if (failedWaypoints.length > 0) { 404 | return { 405 | content: [{ 406 | type: "text", 407 | text: `다음 경유지를 찾을 수 없습니다: ${failedWaypoints.map(wp => wp.name).join(', ')}` 408 | }] 409 | }; 410 | } 411 | 412 | // 4. 결과 조합 413 | const origin = originResponse.data.documents[0]; 414 | const destination = destinationResponse.data.documents[0]; 415 | 416 | // 기본 웹 링크 생성 (카카오맵) 417 | let formattedResult = ""; 418 | let mapUrl = `https://map.kakao.com/?sName=${encodeURIComponent(origin.place_name)}&eName=${encodeURIComponent(destination.place_name)}`; 419 | 420 | // 경유지가 있는 경우 421 | if (waypointsResults.length > 0) { 422 | const successWaypoints = waypointsResults.filter(wp => wp.success && wp.placeName); 423 | if (successWaypoints.length > 0) { 424 | const waypointsParam = successWaypoints 425 | .map(wp => wp.placeName ? encodeURIComponent(wp.placeName) : '') 426 | .filter(Boolean) 427 | .join(','); 428 | 429 | if (waypointsParam) { 430 | mapUrl += `&waypoints=${waypointsParam}`; 431 | } 432 | } 433 | } 434 | 435 | // 이동 수단에 따라 처리 분기 436 | if (params.transportation_type === "car") { 437 | // 자동차 경로는 카카오모빌리티 API 사용 438 | 439 | // 카카오모빌리티 API용 axios 인스턴스 생성 440 | const mobilityApiClient = axios.create({ 441 | headers: { 442 | Authorization: `KakaoAK ${KAKAO_API_KEY}`, 443 | 'Content-Type': 'application/json' 444 | } 445 | }); 446 | 447 | // 카카오모빌리티 API 파라미터 구성 448 | const originCoord = `${origin.x},${origin.y}`; 449 | const destCoord = `${destination.x},${destination.y}`; 450 | 451 | // 경유지 구성 452 | let waypointsParam = ""; 453 | if (waypointsResults.length > 0) { 454 | const successWaypoints = waypointsResults.filter(wp => wp.success && wp.x && wp.y); 455 | if (successWaypoints.length > 0) { 456 | waypointsParam = successWaypoints 457 | .map(wp => `${wp.x},${wp.y}`) 458 | .join('|'); 459 | } 460 | } 461 | 462 | // 카카오모빌리티 API 호출 463 | try { 464 | // <<< 좌표 검색 결과 및 transportation_type 로깅 추가 >>> 465 | const originSuccess = originResponse.data.documents && originResponse.data.documents.length > 0; 466 | const destinationSuccess = destinationResponse.data.documents && destinationResponse.data.documents.length > 0; 467 | logger.log(`Checking conditions for Mobility API call:`); 468 | logger.log(` transportation_type: ${params.transportation_type}`); 469 | logger.log(` origin success: ${originSuccess}`); 470 | logger.log(` destination success: ${destinationSuccess}`); 471 | // (필요 시 waypoints 결과 로깅 추가) 472 | // logger.log(\` waypoints results: \${JSON.stringify(waypointsResults)}\`); 473 | 474 | if (params.transportation_type === "car" && originSuccess && destinationSuccess) { 475 | // 자동차 경로이고, 출발지/목적지 좌표 검색 성공 시에만 모빌리티 API 호출 476 | 477 | // URL 파라미터 구성 478 | const apiParams: Record = { 479 | origin: `${origin.x},${origin.y}`, 480 | destination: `${destination.x},${destination.y}`, 481 | priority: params.priority.toLowerCase(), 482 | car_fuel: "GASOLINE", 483 | alternatives: "false", 484 | road_details: params.traffic_info ? "true" : "false", 485 | summary: "true" 486 | }; 487 | 488 | // 경유지가 있는 경우 추가 489 | if (waypointsParam) { 490 | apiParams.waypoints = waypointsParam; 491 | } 492 | 493 | // 카카오모빌리티 API 호출 (GET 방식으로 변경) 494 | const mobilityResponse = await kakaoMobilityApiClient.get('/v1/directions', { 495 | params: apiParams 496 | }); 497 | 498 | // <<< API 응답 로깅 추가 >>> 499 | logger.log("Kakao Mobility API Response:", JSON.stringify(mobilityResponse.data, null, 2)); 500 | 501 | if (mobilityResponse.data && mobilityResponse.data.routes && mobilityResponse.data.routes.length > 0) { 502 | const route = mobilityResponse.data.routes[0]; 503 | 504 | if (route.result_code === 0) { // 성공 505 | formattedResult = formatMobilityRouteResult(route, origin, destination, waypointsResults, params); 506 | } else { 507 | // 길찾기 실패 시 기본 맵 URL로 대체 508 | formattedResult = formatBasicRouteResult(origin, destination, waypointsResults, params, mapUrl); 509 | } 510 | } else { 511 | // 응답 데이터 없음 - 기본 맵 URL로 대체 512 | formattedResult = formatBasicRouteResult(origin, destination, waypointsResults, params, mapUrl); 513 | } 514 | } else { 515 | // 응답 데이터 없음 - 기본 맵 URL로 대체 516 | formattedResult = formatBasicRouteResult(origin, destination, waypointsResults, params, mapUrl); 517 | } 518 | } catch (error) { 519 | // 더 자세한 오류 로깅 520 | logger.error("Mobility API error:", error); 521 | if (axios.isAxiosError(error)) { 522 | logger.error("API Error details:", error.response?.data); 523 | logger.error("API Error status:", error.response?.status); 524 | } 525 | // API 호출 실패 시 기본 맵 URL로 대체 526 | formattedResult = formatBasicRouteResult(origin, destination, waypointsResults, params, mapUrl); 527 | } 528 | } else { 529 | // 대중교통이나 도보는 기존 방식 사용 530 | const transportMode = params.transportation_type === "public" ? "transit" : "walk"; 531 | mapUrl += `&carMode=${transportMode}`; 532 | formattedResult = formatBasicRouteResult(origin, destination, waypointsResults, params, mapUrl); 533 | } 534 | 535 | return { 536 | content: [{ type: "text", text: formattedResult }] 537 | }; 538 | } catch (error: unknown) { 539 | let errorMessage = "알 수 없는 오류 발생"; 540 | if (axios.isAxiosError(error)) { 541 | const axiosError = error as AxiosError<{ message?: string }>; 542 | logger.error("API Error status:", axiosError.response?.status); 543 | errorMessage = axiosError.response?.data?.message || axiosError.message; 544 | } else if (error instanceof Error) { 545 | errorMessage = error.message; 546 | } 547 | return { 548 | content: [{ type: "text", text: `길찾기 중 오류 발생: ${errorMessage}` }] 549 | }; 550 | } 551 | } 552 | ); 553 | 554 | // Daum 웹 검색 도구 555 | const webSearchSchema = z.object({ 556 | query: z.string().describe('검색할 질의어'), 557 | sort: z.enum(['accuracy', 'recency']).optional().describe('결과 정렬 방식 (accuracy: 정확도순, recency: 최신순)'), 558 | page: z.number().int().min(1).max(50).optional().describe('결과 페이지 번호 (1~50, 기본값 1)'), 559 | size: z.number().int().min(1).max(50).optional().describe('한 페이지에 보여질 문서 수 (1~50, 기본값 10)'), 560 | }); 561 | 562 | server.tool( 563 | "search-web", 564 | "다음(Daum) 검색에서 웹 문서를 검색합니다.", 565 | webSearchSchema.shape, 566 | async (params: z.infer) => { 567 | logger.log("Executing search-web tool with params:", params); 568 | try { 569 | const response = await kakaoApiClient.get( 570 | "/v2/search/web", 571 | { 572 | params: { 573 | query: params.query, 574 | sort: params.sort, 575 | page: params.page, 576 | size: params.size, 577 | }, 578 | } 579 | ); 580 | 581 | logger.log(`API Response received: ${response.status}`); 582 | logger.log(`Results count: ${response.data.meta.total_count}`); 583 | 584 | const formattedResponse = formatDaumSearchResponse("웹 문서", response.data); 585 | 586 | return { 587 | content: [{ type: "text", text: formattedResponse }], 588 | }; 589 | } catch (error: unknown) { 590 | let errorMessage = "알 수 없는 오류 발생"; 591 | if (axios.isAxiosError(error)) { 592 | const axiosError = error as AxiosError<{ message?: string }>; 593 | logger.error("API Error status:", axiosError.response?.status); 594 | logger.error("API Error details:", JSON.stringify(axiosError.response?.data)); 595 | errorMessage = axiosError.response?.data?.message || axiosError.message; 596 | } else if (error instanceof Error) { 597 | errorMessage = error.message; 598 | } 599 | 600 | return { 601 | content: [{ type: "text", text: `웹 검색 중 오류 발생: ${errorMessage}` }], 602 | }; 603 | } 604 | } 605 | ); 606 | 607 | // Daum 이미지 검색 도구 608 | const imageSearchSchema = z.object({ 609 | query: z.string().describe('검색할 질의어'), 610 | sort: z.enum(['accuracy', 'recency']).optional().describe('결과 정렬 방식 (accuracy: 정확도순, recency: 최신순)'), 611 | page: z.number().int().min(1).max(50).optional().describe('결과 페이지 번호 (1~50, 기본값 1)'), 612 | size: z.number().int().min(1).max(80).optional().describe('한 페이지에 보여질 문서 수 (1~80, 기본값 10)'), 613 | }); 614 | 615 | server.tool( 616 | "search-image", 617 | "다음(Daum) 검색에서 이미지를 검색합니다.", 618 | imageSearchSchema.shape, 619 | async (params: z.infer) => { 620 | logger.log("Executing search-image tool with params:", params); 621 | try { 622 | const response = await kakaoApiClient.get( 623 | "/v2/search/image", 624 | { 625 | params: { 626 | query: params.query, 627 | sort: params.sort, 628 | page: params.page, 629 | size: params.size, 630 | }, 631 | } 632 | ); 633 | 634 | logger.log(`API Response received: ${response.status}`); 635 | logger.log(`Results count: ${response.data.meta.total_count}`); 636 | 637 | const formattedResponse = formatDaumSearchResponse("이미지", response.data); 638 | 639 | return { 640 | content: [{ type: "text", text: formattedResponse }], 641 | }; 642 | } catch (error: unknown) { 643 | let errorMessage = "알 수 없는 오류 발생"; 644 | if (axios.isAxiosError(error)) { 645 | const axiosError = error as AxiosError<{ message?: string }>; 646 | logger.error("API Error status:", axiosError.response?.status); 647 | logger.error("API Error details:", JSON.stringify(axiosError.response?.data)); 648 | errorMessage = axiosError.response?.data?.message || axiosError.message; 649 | } else if (error instanceof Error) { 650 | errorMessage = error.message; 651 | } 652 | 653 | return { 654 | content: [{ type: "text", text: `이미지 검색 중 오류 발생: ${errorMessage}` }], 655 | }; 656 | } 657 | } 658 | ); 659 | 660 | // Daum 블로그 검색 도구 661 | const blogSearchSchema = z.object({ 662 | query: z.string().describe('검색할 질의어'), 663 | sort: z.enum(['accuracy', 'recency']).optional().describe('결과 정렬 방식 (accuracy: 정확도순, recency: 최신순)'), 664 | page: z.number().int().min(1).max(50).optional().describe('결과 페이지 번호 (1~50, 기본값 1)'), 665 | size: z.number().int().min(1).max(50).optional().describe('한 페이지에 보여질 문서 수 (1~50, 기본값 10)'), 666 | }); 667 | 668 | server.tool( 669 | "search-blog", 670 | "다음(Daum) 검색에서 블로그 글을 검색합니다.", 671 | blogSearchSchema.shape, 672 | async (params: z.infer) => { 673 | logger.log("Executing search-blog tool with params:", params); 674 | try { 675 | const response = await kakaoApiClient.get( 676 | "/v2/search/blog", 677 | { 678 | params: { 679 | query: params.query, 680 | sort: params.sort, 681 | page: params.page, 682 | size: params.size, 683 | }, 684 | } 685 | ); 686 | 687 | logger.log(`API Response received: ${response.status}`); 688 | logger.log(`Results count: ${response.data.meta.total_count}`); 689 | 690 | const formattedResponse = formatDaumSearchResponse("블로그", response.data); 691 | 692 | return { 693 | content: [{ type: "text", text: formattedResponse }], 694 | }; 695 | } catch (error: unknown) { 696 | let errorMessage = "알 수 없는 오류 발생"; 697 | if (axios.isAxiosError(error)) { 698 | const axiosError = error as AxiosError<{ message?: string }>; 699 | logger.error("API Error status:", axiosError.response?.status); 700 | logger.error("API Error details:", JSON.stringify(axiosError.response?.data)); 701 | errorMessage = axiosError.response?.data?.message || axiosError.message; 702 | } else if (error instanceof Error) { 703 | errorMessage = error.message; 704 | } 705 | 706 | return { 707 | content: [{ type: "text", text: `블로그 검색 중 오류 발생: ${errorMessage}` }], 708 | }; 709 | } 710 | } 711 | ); 712 | 713 | // Daum 카페 검색 도구 714 | const cafeSearchSchema = z.object({ 715 | query: z.string().describe('검색할 질의어'), 716 | sort: z.enum(['accuracy', 'recency']).optional().describe('결과 정렬 방식 (accuracy: 정확도순, recency: 최신순)'), 717 | page: z.number().int().min(1).max(50).optional().describe('결과 페이지 번호 (1~50, 기본값 1)'), 718 | size: z.number().int().min(1).max(50).optional().describe('한 페이지에 보여질 문서 수 (1~50, 기본값 10)'), 719 | }); 720 | 721 | server.tool( 722 | "search-cafe", 723 | "다음(Daum) 검색에서 카페 글을 검색합니다.", 724 | cafeSearchSchema.shape, 725 | async (params: z.infer) => { 726 | logger.log("Executing search-cafe tool with params:", params); 727 | try { 728 | const response = await kakaoApiClient.get( 729 | "/v2/search/cafe", 730 | { 731 | params: { 732 | query: params.query, 733 | sort: params.sort, 734 | page: params.page, 735 | size: params.size, 736 | }, 737 | } 738 | ); 739 | 740 | logger.log(`API Response received: ${response.status}`); 741 | logger.log(`Results count: ${response.data.meta.total_count}`); 742 | 743 | const formattedResponse = formatDaumSearchResponse("카페", response.data); 744 | 745 | return { 746 | content: [{ type: "text", text: formattedResponse }], 747 | }; 748 | } catch (error: unknown) { 749 | let errorMessage = "알 수 없는 오류 발생"; 750 | if (axios.isAxiosError(error)) { 751 | const axiosError = error as AxiosError<{ message?: string }>; 752 | logger.error("API Error status:", axiosError.response?.status); 753 | logger.error("API Error details:", JSON.stringify(axiosError.response?.data)); 754 | errorMessage = axiosError.response?.data?.message || axiosError.message; 755 | } else if (error instanceof Error) { 756 | errorMessage = error.message; 757 | } 758 | 759 | return { 760 | content: [{ type: "text", text: `카페 검색 중 오류 발생: ${errorMessage}` }], 761 | }; 762 | } 763 | } 764 | ); 765 | 766 | // --- Helper Functions --- 767 | 768 | function formatPlacesResponse(data: KakaoKeywordSearchResponse): string { 769 | if (!data || !data.documents || data.documents.length === 0) { 770 | return "검색 결과가 없습니다."; 771 | } 772 | const places = data.documents.map((place: KakaoPlaceDocument) => { 773 | let result = `이름: ${place.place_name}\n주소: ${place.address_name}\n카테고리: ${place.category_name}`; 774 | 775 | // 전화번호가 있으면 추가 776 | if (place.phone) { 777 | result += `\n전화번호: ${place.phone}`; 778 | } 779 | 780 | // 카카오맵 링크 추가 781 | result += `\n상세정보: ${place.place_url}`; 782 | 783 | return result; 784 | }).join("\n---\n"); 785 | 786 | const pageInfo = data.meta ? ` (결과 수: ${data.meta.pageable_count}, 총 ${data.meta.total_count}개)` : ""; 787 | return `장소 검색 결과${pageInfo}:\n${places}`; 788 | } 789 | 790 | function formatAddressResponse(data: KakaoCoord2AddressResponse): string { 791 | if (!data || !data.documents || data.documents.length === 0) { 792 | return "해당 좌표에 대한 주소 정보를 찾을 수 없습니다."; 793 | } 794 | const doc = data.documents[0]; 795 | const roadAddress = doc.road_address ? `도로명: ${doc.road_address.address_name}` : "도로명 주소 정보 없음"; 796 | const lotAddress = doc.address ? `지번: ${doc.address.address_name}` : "지번 주소 정보 없음"; 797 | return `주소 변환 결과:\n${roadAddress}\n${lotAddress}`; 798 | } 799 | 800 | // 카카오모빌리티 API 응답을 포맷팅하는 함수 801 | function formatMobilityRouteResult( 802 | route: Record, 803 | origin: KakaoPlaceDocument, 804 | destination: KakaoPlaceDocument, 805 | waypoints: WaypointResult[], 806 | params: z.infer 807 | ): string { 808 | let result = '🗺️ 길찾기 결과\\n\\n'; 809 | result += `출발지: ${origin.place_name} (${origin.address_name})\\n`; 810 | 811 | // 경유지가 있는 경우 812 | if (waypoints.length > 0) { 813 | const successWaypoints = waypoints.filter(wp => wp.success); 814 | if (successWaypoints.length > 0) { 815 | result += '\\n경유지:\\n'; 816 | for (const [index, wp] of successWaypoints.entries()) { // forEach 대신 for...of 사용 817 | result += `${index + 1}. ${wp.placeName} (${wp.addressName})\\n`; 818 | } 819 | } 820 | } 821 | 822 | result += `\\n목적지: ${destination.place_name} (${destination.address_name})\\n`; 823 | result += `\\n이동 수단: ${getTransportationName(params.transportation_type)}\\n`; 824 | 825 | // 카카오모빌리티 API 결과 표시 826 | const summary = route.summary; 827 | if (summary && typeof summary === 'object') { // summary 타입 확인 추가 828 | if (typeof summary.distance === 'number') { // distance 타입 확인 추가 829 | result += `\\n총 거리: ${formatDistance(summary.distance)}\\n`; 830 | } 831 | if (typeof summary.duration === 'number') { // duration 타입 확인 추가 832 | result += `예상 소요 시간: ${formatDuration(summary.duration)}\\n`; 833 | } 834 | 835 | // 택시 요금 표시 836 | if (summary.fare && typeof summary.fare === 'object' && typeof summary.fare.taxi === 'number') { // 타입 확인 추가 837 | result += `예상 택시 요금: ${summary.fare.taxi.toLocaleString()}원\\n`; 838 | } 839 | 840 | // 통행 요금 표시 841 | if (summary.fare && typeof summary.fare === 'object' && typeof summary.fare.toll === 'number' && summary.fare.toll > 0) { // 타입 확인 추가 842 | result += `통행 요금: ${summary.fare.toll.toLocaleString()}원\\n`; 843 | } 844 | } 845 | 846 | // 교통 정보 표시 847 | if (params.traffic_info && Array.isArray(route.sections)) { // sections 타입 확인 추가 848 | result += '\\n📊 교통 상황 요약:\\n'; 849 | 850 | let totalDistance = 0; 851 | let totalCongestionDistance = 0; 852 | let totalHeavyDistance = 0; 853 | let totalSlowDistance = 0; 854 | 855 | // 타입 단언 대신 타입 가드 사용 (더 안전한 방식은 API 응답 타입 정의) 856 | for (const section of route.sections) { 857 | if (section && typeof section === 'object' && Array.isArray(section.roads)) { 858 | for (const road of section.roads) { 859 | if (road && typeof road === 'object' && typeof road.distance === 'number' && typeof road.traffic_state === 'number') { 860 | totalDistance += road.distance; 861 | if (road.traffic_state === 4) { 862 | totalCongestionDistance += road.distance; 863 | } else if (road.traffic_state === 3) { 864 | totalHeavyDistance += road.distance; 865 | } else if (road.traffic_state === 2) { 866 | totalSlowDistance += road.distance; 867 | } 868 | } 869 | } 870 | } 871 | } 872 | 873 | // 전체 거리 중 교통 상태별 비율 계산 874 | if (totalDistance > 0) { 875 | const congestionPercent = Math.round((totalCongestionDistance / totalDistance) * 100); 876 | const heavyPercent = Math.round((totalHeavyDistance / totalDistance) * 100); 877 | const slowPercent = Math.round((totalSlowDistance / totalDistance) * 100); 878 | const smoothPercent = 100 - congestionPercent - heavyPercent - slowPercent; 879 | 880 | result += `🟢 원활: ${smoothPercent}%\\n`; 881 | result += `🟡 서행: ${slowPercent}%\\n`; 882 | result += `🟠 지체: ${heavyPercent}%\\n`; 883 | result += `🔴 정체: ${congestionPercent}%\\n`; 884 | } 885 | 886 | // 주요 정체 구간 표시 (최대 3개) 887 | if (Array.isArray(route.sections) && params.traffic_info) { // sections 타입 확인 추가 888 | const congestionRoads: { name: string; distance: number; traffic_state: number }[] = []; 889 | 890 | for (const section of route.sections) { 891 | if (section && typeof section === 'object' && Array.isArray(section.roads)) { 892 | for (const road of section.roads) { 893 | if (road && typeof road === 'object' && typeof road.traffic_state === 'number' && road.traffic_state >= 3 && typeof road.distance === 'number' && road.distance > 300 && typeof road.name === 'string') { 894 | congestionRoads.push({ 895 | name: road.name, 896 | distance: road.distance, 897 | traffic_state: road.traffic_state 898 | }); 899 | } 900 | } 901 | } 902 | } 903 | 904 | congestionRoads.sort((a, b) => b.distance - a.distance); 905 | 906 | if (congestionRoads.length > 0) { 907 | result += '\\n주요 정체 구간:\\n'; 908 | for(const road of congestionRoads.slice(0, 3)) { 909 | const trafficEmoji = road.traffic_state === 4 ? '🔴' : '🟠'; 910 | result += `${trafficEmoji} ${road.name} (${formatDistance(road.distance)})\\n`; 911 | } 912 | } 913 | } 914 | } 915 | 916 | // 카카오맵 링크 917 | const mapUrl = `https://map.kakao.com/?sName=${encodeURIComponent(origin.place_name)}&eName=${encodeURIComponent(destination.place_name)}`; 918 | result += `\\n카카오맵에서 보기: ${mapUrl}\\n`; 919 | 920 | return result; 921 | } 922 | 923 | // 기본 경로 결과 포맷팅 함수 924 | function formatBasicRouteResult( 925 | origin: KakaoPlaceDocument, 926 | destination: KakaoPlaceDocument, 927 | waypoints: WaypointResult[], 928 | params: z.infer, 929 | mapUrl: string 930 | ): string { 931 | let result = '🗺️ 길찾기 결과\\n\\n'; 932 | result += `출발지: ${origin.place_name} (${origin.address_name})\\n`; 933 | 934 | if (waypoints.length > 0) { 935 | const successWaypoints = waypoints.filter(wp => wp.success && wp.placeName && wp.addressName); 936 | if (successWaypoints.length > 0) { 937 | result += '\\n경유지:\\n'; 938 | successWaypoints.forEach((wp, index) => { 939 | result += `${index + 1}. ${wp.placeName} (${wp.addressName})\\n`; 940 | }); 941 | } 942 | } 943 | 944 | result += `\\n목적지: ${destination.place_name} (${destination.address_name})\\n`; 945 | result += `\\n이동 수단: ${getTransportationName(params.transportation_type)}\\n`; 946 | result += `\\n카카오맵 길찾기: ${mapUrl}\\n`; 947 | result += '\\n상세 경로 및 소요 시간은 카카오맵 링크를 통해 확인하세요.'; 948 | 949 | return result; 950 | } 951 | 952 | // 거리를 포맷팅하는 함수 953 | function formatDistance(meters: number): string { 954 | if (meters < 1000) { 955 | return `${meters}m`; 956 | } 957 | return `${(meters / 1000).toFixed(1)}km`; 958 | } 959 | 960 | // 시간을 포맷팅하는 함수 961 | function formatDuration(seconds: number): string { 962 | const hours = Math.floor(seconds / 3600); 963 | const minutes = Math.floor((seconds % 3600) / 60); 964 | 965 | if (hours > 0) { 966 | return `${hours}시간 ${minutes}분`; 967 | } 968 | return `${minutes}분`; 969 | } 970 | 971 | // 이동 수단 한글 이름 반환 함수 972 | function getTransportationName(type: string): string { 973 | switch (type) { 974 | case "car": 975 | return "자동차"; 976 | case "public": 977 | return "대중교통"; 978 | case "walk": 979 | return "도보"; 980 | default: 981 | return "자동차"; 982 | } 983 | } 984 | 985 | // 다음 검색 결과 포맷팅 함수 986 | function formatDaumSearchResponse(searchType: string, data: DaumSearchResponse): string { 987 | if (!data || !data.documents || data.documents.length === 0) { 988 | return '검색 결과가 없습니다.'; 989 | } 990 | 991 | let result = `${searchType} 검색 결과 (총 ${data.meta.total_count}개 중 ${data.documents.length}개 표시):\n\n`; 992 | 993 | for (const [index, doc] of data.documents.entries()) { 994 | result += `${index + 1}. `; 995 | 996 | // 제목 처리 997 | const title = doc.title; 998 | const sitename = doc.display_sitename; 999 | if (typeof title === 'string' && title) { 1000 | result += `${title.replace(//g, '').replace(/<\/b>/g, '')}\n`; 1001 | } else if (typeof sitename === 'string' && sitename) { 1002 | result += `${sitename}\n`; 1003 | } else { 1004 | result += '[제목 없음]\n'; 1005 | } 1006 | 1007 | // 내용 처리 1008 | const contents = doc.contents; 1009 | if (typeof contents === 'string' && contents) { 1010 | const cleanContent = contents.replace(//g, '').replace(/<\/b>/g, ''); 1011 | result += ` 내용: ${cleanContent.substring(0, 100)}${cleanContent.length > 100 ? '...' : ''}\n`; 1012 | } 1013 | 1014 | // URL 처리 1015 | if (typeof doc.url === 'string' && doc.url) { 1016 | result += ` URL: ${doc.url}\n`; 1017 | } 1018 | 1019 | // 날짜 처리 1020 | const datetimeValue = doc.datetime; 1021 | if (typeof datetimeValue === 'string' || typeof datetimeValue === 'number') { 1022 | try { 1023 | const datetime = new Date(datetimeValue); 1024 | if (!Number.isNaN(datetime.getTime())) { // isNaN 대신 Number.isNaN 사용 및 유효성 검사 강화 1025 | result += ` 날짜: ${datetime.toLocaleDateString('ko-KR')}\n`; 1026 | } 1027 | } catch (e) { 1028 | logger.error(`Invalid date format for datetime: ${datetimeValue}`); 1029 | } 1030 | } 1031 | 1032 | // 이미지 URL 처리 1033 | const thumbnailUrl = doc.thumbnail_url; 1034 | const imageUrl = doc.image_url; 1035 | if (typeof thumbnailUrl === 'string' || typeof imageUrl === 'string') { 1036 | result += ` 이미지: ${thumbnailUrl || imageUrl}\n`; 1037 | } 1038 | 1039 | // 카페명 처리 1040 | if (typeof doc.cafename === 'string' && doc.cafename) { 1041 | result += ` 카페: ${doc.cafename}\n`; 1042 | } 1043 | 1044 | // 블로그명 처리 1045 | if (typeof doc.blogname === 'string' && doc.blogname) { 1046 | result += ` 블로그: ${doc.blogname}\n`; 1047 | } 1048 | 1049 | // 출처 처리 1050 | if (typeof doc.collection === 'string' && doc.collection) { 1051 | result += ` 출처: ${doc.collection}\n`; 1052 | } 1053 | 1054 | // 크기 처리 1055 | if (typeof doc.width === 'number' && typeof doc.height === 'number') { 1056 | result += ` 크기: ${doc.width}x${doc.height}\n`; 1057 | } 1058 | 1059 | result += '\n'; // 각 문서 사이에 줄바꿈 추가 1060 | } 1061 | 1062 | // 페이지 정보 추가 1063 | result += `현재 페이지가 마지막 페이지${data.meta.is_end ? '입니다.' : '가 아닙니다. 더 많은 결과를 보려면 page 매개변수를 증가시키세요.'}\n`; 1064 | 1065 | return result; // 함수 끝에 return 명시 1066 | } 1067 | 1068 | // --- Run the Server (based on mode) --- 1069 | 1070 | // Export the server instance if needed for testing or other modules 1071 | // export { server }; // <-- 파일 최상단으로 이동하거나 제거 (Node.js ESM 권장사항 따름) 1072 | 1073 | // ESM에서는 require.main === module 대신 다른 방식으로 직접 실행 감지 1074 | // https://nodejs.org/api/esm.html#esm_no_require_exports_module_exports_filename_dirname 1075 | // Node.js v20부터는 import.meta.url을 사용하여 현재 파일이 직접 실행되는지 확인할 수 있음 1076 | // const isMainModule = import.meta.url === \`file://\${process.argv[1]}\`; // 수정 전 1077 | let isMainModule = false; 1078 | try { 1079 | // fileURLToPath를 사용하여 올바르게 경로 비교 1080 | const currentFilePath = fileURLToPath(import.meta.url); 1081 | isMainModule = currentFilePath === process.argv[1]; 1082 | } catch (e) { 1083 | // import.meta.url이 지원되지 않는 환경 고려 (예: CommonJS) 1084 | // 또는 다른 방식으로 메인 모듈 여부 확인 필요 시 추가 1085 | } 1086 | 1087 | // import/export는 최상위 레벨에서만 사용 가능하므로, 서버 시작 로직을 함수로 감싸기 1088 | async function startServer() { 1089 | const mode = argv.mode as 'stdio' | 'http'; 1090 | const port = argv.port as number; 1091 | 1092 | if (mode === 'stdio') { 1093 | // STDIO Mode - Direct connection via stdio 1094 | logger.log("Starting Kakao Map MCP Server in stdio mode..."); 1095 | 1096 | const stdioTransport = new StdioServerTransport(); 1097 | 1098 | // 디버깅을 위한 stdio 입력 로깅 1099 | process.stdin.on('data', (data) => { 1100 | const input = data.toString().trim(); 1101 | logger.log(`STDIN received: ${input}`); // logger.log로 복원 1102 | // logger.error(\`STDIN received (error level): \${input}\`); // logger.error 주석 처리 1103 | 1104 | try { 1105 | // 입력 데이터 파싱 1106 | const parsedData = JSON.parse(input); 1107 | logger.log(`Parsed message type: ${parsedData.type}, tool: ${parsedData.tool}`); // logger.log로 복원 1108 | // logger.error(\`Parsed message (error level) - Type: \${parsedData.type}, Tool: \${parsedData.tool}\`); // logger.error 주석 처리 1109 | } catch (err: unknown) { 1110 | if (err instanceof Error) { 1111 | logger.error(`Failed to parse input: ${err.message}`); 1112 | } else { 1113 | logger.error('Failed to parse input: Unknown error'); // 수정 후 1114 | } 1115 | } 1116 | }); 1117 | 1118 | server.connect(stdioTransport).then(() => { 1119 | logger.log("Kakao Map MCP Server connected via stdio."); 1120 | }).catch(error => { 1121 | logger.error(`Failed to connect server via stdio: ${error}`); 1122 | process.exit(1); 1123 | }); 1124 | } else { 1125 | // HTTP/SSE Mode - Express server with SSE 1126 | const app = express(); 1127 | let sseTransport: SSEServerTransport | null = null; 1128 | 1129 | // CORS Configuration 1130 | const corsOptions = { 1131 | origin: 'http://localhost:5173', // For MCP Inspector 1132 | methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", 1133 | allowedHeaders: ["Content-Type", "Authorization"], 1134 | credentials: true, 1135 | optionsSuccessStatus: 204 1136 | }; 1137 | app.use(cors(corsOptions)); 1138 | 1139 | // Routes 1140 | app.get("/sse", async (req: Request, res: Response) => { 1141 | logger.log("New SSE connection request received."); 1142 | 1143 | if (sseTransport) { 1144 | logger.log("An existing SSE transport is active. Replacing with the new connection."); 1145 | } 1146 | 1147 | // Create a new transport for this request 1148 | const currentTransport = new SSEServerTransport("/messages", res as unknown as ServerResponse); 1149 | sseTransport = currentTransport; 1150 | 1151 | try { 1152 | await server.connect(sseTransport); 1153 | logger.log("MCP Server connected to SSE transport."); 1154 | 1155 | req.on("close", () => { 1156 | logger.log("SSE connection closed by client."); 1157 | if (sseTransport === currentTransport) { 1158 | sseTransport = null; 1159 | } 1160 | }); 1161 | } catch (error) { 1162 | logger.error(`Error connecting MCP server to SSE transport: ${error}`); 1163 | if (sseTransport === currentTransport) { 1164 | sseTransport = null; 1165 | } 1166 | if (!res.writableEnded) { 1167 | res.status(500).end(); 1168 | } 1169 | } 1170 | }); 1171 | 1172 | app.post("/messages", (req: Request, res: Response): void => { 1173 | logger.log("Received POST /messages request."); 1174 | if (!sseTransport) { 1175 | logger.error("Received POST message but no active SSE transport."); 1176 | res.status(400).send("No active SSE connection"); 1177 | return; 1178 | } 1179 | 1180 | sseTransport.handlePostMessage(req as unknown as IncomingMessage, res as unknown as ServerResponse) 1181 | .then(() => { 1182 | logger.log("POST message handled by SSE transport."); 1183 | }) 1184 | .catch((error) => { 1185 | logger.error(`Error handling POST message: ${error}`); 1186 | if (!res.headersSent) { 1187 | res.status(500).send("Error processing message"); 1188 | } 1189 | }); 1190 | }); 1191 | 1192 | // Start Server 1193 | app.listen(port, () => { 1194 | logger.log(`Kakao Map MCP Server (HTTP/SSE) listening on port ${port}`); 1195 | logger.log(`SSE endpoint available at http://localhost:${port}/sse`); 1196 | logger.log(`Message endpoint available at http://localhost:${port}/messages`); 1197 | }); 1198 | } 1199 | } // startServer 함수 닫는 중괄호 1200 | 1201 | // 메인 모듈로 실행될 때만 서버 시작 1202 | if (isMainModule) { 1203 | startServer().catch(error => { 1204 | logger.error("Failed to start server:", error); 1205 | process.exit(1); 1206 | }); 1207 | } // if (isMainModule) 닫는 중괄호 1208 | 1209 | // 서버 인스턴스를 export해야 한다면 파일 최상단에서 export 1210 | export { server }; 1211 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["es6","dom"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "libReplacement": true, /* Enable lib replacement. */ 18 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 19 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 20 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 21 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 22 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 23 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 24 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 25 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 26 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 27 | 28 | /* Modules */ 29 | "module": "NodeNext", 30 | "rootDir": "src", 31 | "moduleResolution": "NodeNext", 32 | // "baseUrl": "./", 33 | // "paths": {}, 34 | // "rootDirs": [], 35 | // "typeRoots": [], 36 | // "types": [], 37 | // "allowUmdGlobalAccess": true, 38 | // "moduleSuffixes": [], 39 | // "allowImportingTsExtensions": true, 40 | // "rewriteRelativeImportExtensions": true, 41 | // "resolvePackageJsonExports": true, 42 | // "resolvePackageJsonImports": true, 43 | // "customConditions": [], 44 | // "noUncheckedSideEffectImports": true, 45 | "resolveJsonModule": true, 46 | // "allowArbitraryExtensions": true, 47 | // "noResolve": true, 48 | 49 | /* JavaScript Support */ 50 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 51 | // "checkJs": true, 52 | // "maxNodeModuleJsDepth": 1, 53 | 54 | /* Emit */ 55 | // "declaration": true, 56 | // "declarationMap": true, 57 | // "emitDeclarationOnly": true, 58 | // "sourceMap": true, 59 | // "inlineSourceMap": true, 60 | // "noEmit": true, 61 | // "outFile": "./", 62 | "outDir": "dist", 63 | // "removeComments": true, 64 | // "importHelpers": true, 65 | // "downlevelIteration": true, 66 | // "sourceRoot": "", 67 | // "mapRoot": "", 68 | // "inlineSources": true, 69 | // "emitBOM": true, 70 | // "newLine": "crlf", 71 | // "stripInternal": true, 72 | // "noEmitHelpers": true, 73 | // "noEmitOnError": true, 74 | // "preserveConstEnums": true, 75 | // "declarationDir": "./", 76 | 77 | /* Interop Constraints */ 78 | // "isolatedModules": true, 79 | // "verbatimModuleSyntax": true, 80 | // "isolatedDeclarations": true, 81 | // "erasableSyntaxOnly": true, 82 | // "allowSyntheticDefaultImports": true, 83 | "esModuleInterop": true, 84 | // "preserveSymlinks": true, 85 | "forceConsistentCasingInFileNames": true, 86 | 87 | /* Type Checking */ 88 | "strict": true, 89 | "noImplicitAny": true, 90 | // "strictNullChecks": true, 91 | // "strictFunctionTypes": true, 92 | // "strictBindCallApply": true, 93 | // "strictPropertyInitialization": true, 94 | // "strictBuiltinIteratorReturn": true, 95 | // "noImplicitThis": true, 96 | // "useUnknownInCatchVariables": true, 97 | // "alwaysStrict": true, 98 | // "noUnusedLocals": true, 99 | // "noUnusedParameters": true, 100 | // "exactOptionalPropertyTypes": true, 101 | // "noImplicitReturns": true, 102 | // "noFallthroughCasesInSwitch": true, 103 | // "noUncheckedIndexedAccess": true, 104 | // "noImplicitOverride": true, 105 | // "noPropertyAccessFromIndexSignature": true, 106 | // "allowUnusedLabels": true, 107 | // "allowUnreachableCode": true, 108 | 109 | /* Completeness */ 110 | // "skipDefaultLibCheck": true, 111 | "skipLibCheck": true 112 | }, 113 | "include": ["src/**/*"], 114 | "exclude": ["node_modules", "dist", "test.js", "test-request.json", "**/*.spec.ts"] 115 | } 116 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@modelcontextprotocol/sdk@^1.8.0": 6 | version "1.8.0" 7 | resolved "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz" 8 | integrity sha512-e06W7SwrontJDHwCawNO5SGxG+nU9AAx+jpHHZqGl/WrDBdWOpvirC+s58VpJTB5QemI4jTRcjWT4Pt3Q1NPQQ== 9 | dependencies: 10 | content-type "^1.0.5" 11 | cors "^2.8.5" 12 | cross-spawn "^7.0.3" 13 | eventsource "^3.0.2" 14 | express "^5.0.1" 15 | express-rate-limit "^7.5.0" 16 | pkce-challenge "^4.1.0" 17 | raw-body "^3.0.0" 18 | zod "^3.23.8" 19 | zod-to-json-schema "^3.24.1" 20 | 21 | "@types/body-parser@*": 22 | version "1.19.5" 23 | resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" 24 | integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== 25 | dependencies: 26 | "@types/connect" "*" 27 | "@types/node" "*" 28 | 29 | "@types/connect@*": 30 | version "3.4.38" 31 | resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz" 32 | integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== 33 | dependencies: 34 | "@types/node" "*" 35 | 36 | "@types/cors@^2.8.17": 37 | version "2.8.17" 38 | resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz" 39 | integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== 40 | dependencies: 41 | "@types/node" "*" 42 | 43 | "@types/express-serve-static-core@^5.0.0": 44 | version "5.0.6" 45 | resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz" 46 | integrity sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA== 47 | dependencies: 48 | "@types/node" "*" 49 | "@types/qs" "*" 50 | "@types/range-parser" "*" 51 | "@types/send" "*" 52 | 53 | "@types/express@^5.0.1": 54 | version "5.0.1" 55 | resolved "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz" 56 | integrity sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ== 57 | dependencies: 58 | "@types/body-parser" "*" 59 | "@types/express-serve-static-core" "^5.0.0" 60 | "@types/serve-static" "*" 61 | 62 | "@types/http-errors@*": 63 | version "2.0.4" 64 | resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" 65 | integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== 66 | 67 | "@types/mime@^1": 68 | version "1.3.5" 69 | resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz" 70 | integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== 71 | 72 | "@types/node@*": 73 | version "22.14.0" 74 | resolved "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz" 75 | integrity sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA== 76 | dependencies: 77 | undici-types "~6.21.0" 78 | 79 | "@types/qs@*": 80 | version "6.9.18" 81 | resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz" 82 | integrity sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA== 83 | 84 | "@types/range-parser@*": 85 | version "1.2.7" 86 | resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz" 87 | integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== 88 | 89 | "@types/send@*": 90 | version "0.17.4" 91 | resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" 92 | integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== 93 | dependencies: 94 | "@types/mime" "^1" 95 | "@types/node" "*" 96 | 97 | "@types/serve-static@*": 98 | version "1.15.7" 99 | resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz" 100 | integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== 101 | dependencies: 102 | "@types/http-errors" "*" 103 | "@types/node" "*" 104 | "@types/send" "*" 105 | 106 | accepts@^2.0.0: 107 | version "2.0.0" 108 | resolved "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz" 109 | integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== 110 | dependencies: 111 | mime-types "^3.0.0" 112 | negotiator "^1.0.0" 113 | 114 | asynckit@^0.4.0: 115 | version "0.4.0" 116 | resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" 117 | integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== 118 | 119 | axios@^1.8.4: 120 | version "1.8.4" 121 | resolved "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz" 122 | integrity sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw== 123 | dependencies: 124 | follow-redirects "^1.15.6" 125 | form-data "^4.0.0" 126 | proxy-from-env "^1.1.0" 127 | 128 | body-parser@^2.2.0: 129 | version "2.2.0" 130 | resolved "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz" 131 | integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg== 132 | dependencies: 133 | bytes "^3.1.2" 134 | content-type "^1.0.5" 135 | debug "^4.4.0" 136 | http-errors "^2.0.0" 137 | iconv-lite "^0.6.3" 138 | on-finished "^2.4.1" 139 | qs "^6.14.0" 140 | raw-body "^3.0.0" 141 | type-is "^2.0.0" 142 | 143 | bytes@^3.1.2, bytes@3.1.2: 144 | version "3.1.2" 145 | resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" 146 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 147 | 148 | call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: 149 | version "1.0.2" 150 | resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" 151 | integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== 152 | dependencies: 153 | es-errors "^1.3.0" 154 | function-bind "^1.1.2" 155 | 156 | call-bound@^1.0.2: 157 | version "1.0.4" 158 | resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz" 159 | integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== 160 | dependencies: 161 | call-bind-apply-helpers "^1.0.2" 162 | get-intrinsic "^1.3.0" 163 | 164 | combined-stream@^1.0.8: 165 | version "1.0.8" 166 | resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" 167 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 168 | dependencies: 169 | delayed-stream "~1.0.0" 170 | 171 | content-disposition@^1.0.0: 172 | version "1.0.0" 173 | resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz" 174 | integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg== 175 | dependencies: 176 | safe-buffer "5.2.1" 177 | 178 | content-type@^1.0.5: 179 | version "1.0.5" 180 | resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" 181 | integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== 182 | 183 | cookie-signature@^1.2.1: 184 | version "1.2.2" 185 | resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz" 186 | integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== 187 | 188 | cookie@^0.7.1: 189 | version "0.7.2" 190 | resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz" 191 | integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== 192 | 193 | cors@^2.8.5: 194 | version "2.8.5" 195 | resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" 196 | integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 197 | dependencies: 198 | object-assign "^4" 199 | vary "^1" 200 | 201 | cross-spawn@^7.0.3: 202 | version "7.0.6" 203 | resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" 204 | integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== 205 | dependencies: 206 | path-key "^3.1.0" 207 | shebang-command "^2.0.0" 208 | which "^2.0.1" 209 | 210 | data-uri-to-buffer@^4.0.0: 211 | version "4.0.1" 212 | resolved "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz" 213 | integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== 214 | 215 | debug@^4.3.5, debug@^4.4.0: 216 | version "4.4.0" 217 | resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" 218 | integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== 219 | dependencies: 220 | ms "^2.1.3" 221 | 222 | delayed-stream@~1.0.0: 223 | version "1.0.0" 224 | resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" 225 | integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== 226 | 227 | depd@^2.0.0, depd@2.0.0: 228 | version "2.0.0" 229 | resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" 230 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 231 | 232 | dunder-proto@^1.0.1: 233 | version "1.0.1" 234 | resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" 235 | integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== 236 | dependencies: 237 | call-bind-apply-helpers "^1.0.1" 238 | es-errors "^1.3.0" 239 | gopd "^1.2.0" 240 | 241 | ee-first@1.1.1: 242 | version "1.1.1" 243 | resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" 244 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 245 | 246 | encodeurl@^2.0.0: 247 | version "2.0.0" 248 | resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz" 249 | integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== 250 | 251 | es-define-property@^1.0.1: 252 | version "1.0.1" 253 | resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" 254 | integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== 255 | 256 | es-errors@^1.3.0: 257 | version "1.3.0" 258 | resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz" 259 | integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== 260 | 261 | es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: 262 | version "1.1.1" 263 | resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" 264 | integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== 265 | dependencies: 266 | es-errors "^1.3.0" 267 | 268 | es-set-tostringtag@^2.1.0: 269 | version "2.1.0" 270 | resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz" 271 | integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== 272 | dependencies: 273 | es-errors "^1.3.0" 274 | get-intrinsic "^1.2.6" 275 | has-tostringtag "^1.0.2" 276 | hasown "^2.0.2" 277 | 278 | escape-html@^1.0.3: 279 | version "1.0.3" 280 | resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" 281 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 282 | 283 | etag@^1.8.1: 284 | version "1.8.1" 285 | resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" 286 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 287 | 288 | eventsource-parser@^3.0.1: 289 | version "3.0.1" 290 | resolved "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz" 291 | integrity sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA== 292 | 293 | eventsource@^3.0.2, eventsource@^3.0.6: 294 | version "3.0.6" 295 | resolved "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz" 296 | integrity sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA== 297 | dependencies: 298 | eventsource-parser "^3.0.1" 299 | 300 | express-rate-limit@^7.5.0: 301 | version "7.5.0" 302 | resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz" 303 | integrity sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg== 304 | 305 | "express@^4.11 || 5 || ^5.0.0-beta.1", express@^5.0.1, express@^5.1.0: 306 | version "5.1.0" 307 | resolved "https://registry.npmjs.org/express/-/express-5.1.0.tgz" 308 | integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA== 309 | dependencies: 310 | accepts "^2.0.0" 311 | body-parser "^2.2.0" 312 | content-disposition "^1.0.0" 313 | content-type "^1.0.5" 314 | cookie "^0.7.1" 315 | cookie-signature "^1.2.1" 316 | debug "^4.4.0" 317 | encodeurl "^2.0.0" 318 | escape-html "^1.0.3" 319 | etag "^1.8.1" 320 | finalhandler "^2.1.0" 321 | fresh "^2.0.0" 322 | http-errors "^2.0.0" 323 | merge-descriptors "^2.0.0" 324 | mime-types "^3.0.0" 325 | on-finished "^2.4.1" 326 | once "^1.4.0" 327 | parseurl "^1.3.3" 328 | proxy-addr "^2.0.7" 329 | qs "^6.14.0" 330 | range-parser "^1.2.1" 331 | router "^2.2.0" 332 | send "^1.1.0" 333 | serve-static "^2.2.0" 334 | statuses "^2.0.1" 335 | type-is "^2.0.1" 336 | vary "^1.1.2" 337 | 338 | fetch-blob@^3.1.2, fetch-blob@^3.1.4: 339 | version "3.2.0" 340 | resolved "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz" 341 | integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== 342 | dependencies: 343 | node-domexception "^1.0.0" 344 | web-streams-polyfill "^3.0.3" 345 | 346 | finalhandler@^2.1.0: 347 | version "2.1.0" 348 | resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz" 349 | integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q== 350 | dependencies: 351 | debug "^4.4.0" 352 | encodeurl "^2.0.0" 353 | escape-html "^1.0.3" 354 | on-finished "^2.4.1" 355 | parseurl "^1.3.3" 356 | statuses "^2.0.1" 357 | 358 | follow-redirects@^1.15.6: 359 | version "1.15.9" 360 | resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz" 361 | integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== 362 | 363 | form-data@^4.0.0: 364 | version "4.0.2" 365 | resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz" 366 | integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== 367 | dependencies: 368 | asynckit "^0.4.0" 369 | combined-stream "^1.0.8" 370 | es-set-tostringtag "^2.1.0" 371 | mime-types "^2.1.12" 372 | 373 | formdata-polyfill@^4.0.10: 374 | version "4.0.10" 375 | resolved "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz" 376 | integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== 377 | dependencies: 378 | fetch-blob "^3.1.2" 379 | 380 | forwarded@0.2.0: 381 | version "0.2.0" 382 | resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" 383 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 384 | 385 | fresh@^2.0.0: 386 | version "2.0.0" 387 | resolved "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz" 388 | integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A== 389 | 390 | function-bind@^1.1.2: 391 | version "1.1.2" 392 | resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" 393 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 394 | 395 | get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: 396 | version "1.3.0" 397 | resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" 398 | integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== 399 | dependencies: 400 | call-bind-apply-helpers "^1.0.2" 401 | es-define-property "^1.0.1" 402 | es-errors "^1.3.0" 403 | es-object-atoms "^1.1.1" 404 | function-bind "^1.1.2" 405 | get-proto "^1.0.1" 406 | gopd "^1.2.0" 407 | has-symbols "^1.1.0" 408 | hasown "^2.0.2" 409 | math-intrinsics "^1.1.0" 410 | 411 | get-proto@^1.0.1: 412 | version "1.0.1" 413 | resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" 414 | integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== 415 | dependencies: 416 | dunder-proto "^1.0.1" 417 | es-object-atoms "^1.0.0" 418 | 419 | gopd@^1.2.0: 420 | version "1.2.0" 421 | resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" 422 | integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== 423 | 424 | has-symbols@^1.0.3, has-symbols@^1.1.0: 425 | version "1.1.0" 426 | resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" 427 | integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== 428 | 429 | has-tostringtag@^1.0.2: 430 | version "1.0.2" 431 | resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz" 432 | integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== 433 | dependencies: 434 | has-symbols "^1.0.3" 435 | 436 | hasown@^2.0.2: 437 | version "2.0.2" 438 | resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" 439 | integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== 440 | dependencies: 441 | function-bind "^1.1.2" 442 | 443 | http-errors@^2.0.0, http-errors@2.0.0: 444 | version "2.0.0" 445 | resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" 446 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 447 | dependencies: 448 | depd "2.0.0" 449 | inherits "2.0.4" 450 | setprototypeof "1.2.0" 451 | statuses "2.0.1" 452 | toidentifier "1.0.1" 453 | 454 | iconv-lite@^0.6.3, iconv-lite@0.6.3: 455 | version "0.6.3" 456 | resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" 457 | integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== 458 | dependencies: 459 | safer-buffer ">= 2.1.2 < 3.0.0" 460 | 461 | inherits@2.0.4: 462 | version "2.0.4" 463 | resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" 464 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 465 | 466 | ipaddr.js@1.9.1: 467 | version "1.9.1" 468 | resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" 469 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 470 | 471 | is-promise@^4.0.0: 472 | version "4.0.0" 473 | resolved "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz" 474 | integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== 475 | 476 | isexe@^2.0.0: 477 | version "2.0.0" 478 | resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" 479 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 480 | 481 | math-intrinsics@^1.1.0: 482 | version "1.1.0" 483 | resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" 484 | integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== 485 | 486 | media-typer@^1.1.0: 487 | version "1.1.0" 488 | resolved "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz" 489 | integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== 490 | 491 | merge-descriptors@^2.0.0: 492 | version "2.0.0" 493 | resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz" 494 | integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g== 495 | 496 | mime-db@^1.54.0: 497 | version "1.54.0" 498 | resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz" 499 | integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== 500 | 501 | mime-db@1.52.0: 502 | version "1.52.0" 503 | resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" 504 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 505 | 506 | mime-types@^2.1.12: 507 | version "2.1.35" 508 | resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" 509 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 510 | dependencies: 511 | mime-db "1.52.0" 512 | 513 | mime-types@^3.0.0, mime-types@^3.0.1: 514 | version "3.0.1" 515 | resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz" 516 | integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== 517 | dependencies: 518 | mime-db "^1.54.0" 519 | 520 | ms@^2.1.3: 521 | version "2.1.3" 522 | resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" 523 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 524 | 525 | negotiator@^1.0.0: 526 | version "1.0.0" 527 | resolved "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz" 528 | integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== 529 | 530 | node-domexception@^1.0.0: 531 | version "1.0.0" 532 | resolved "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz" 533 | integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== 534 | 535 | node-fetch@^3.3.2: 536 | version "3.3.2" 537 | resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz" 538 | integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== 539 | dependencies: 540 | data-uri-to-buffer "^4.0.0" 541 | fetch-blob "^3.1.4" 542 | formdata-polyfill "^4.0.10" 543 | 544 | object-assign@^4: 545 | version "4.1.1" 546 | resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" 547 | integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== 548 | 549 | object-inspect@^1.13.3: 550 | version "1.13.4" 551 | resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" 552 | integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== 553 | 554 | on-finished@^2.4.1: 555 | version "2.4.1" 556 | resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" 557 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 558 | dependencies: 559 | ee-first "1.1.1" 560 | 561 | once@^1.4.0: 562 | version "1.4.0" 563 | resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" 564 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 565 | dependencies: 566 | wrappy "1" 567 | 568 | parseurl@^1.3.3: 569 | version "1.3.3" 570 | resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" 571 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 572 | 573 | path-key@^3.1.0: 574 | version "3.1.1" 575 | resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" 576 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 577 | 578 | path-to-regexp@^8.0.0: 579 | version "8.2.0" 580 | resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz" 581 | integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== 582 | 583 | pkce-challenge@^4.1.0: 584 | version "4.1.0" 585 | resolved "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz" 586 | integrity sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ== 587 | 588 | proxy-addr@^2.0.7: 589 | version "2.0.7" 590 | resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" 591 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 592 | dependencies: 593 | forwarded "0.2.0" 594 | ipaddr.js "1.9.1" 595 | 596 | proxy-from-env@^1.1.0: 597 | version "1.1.0" 598 | resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" 599 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 600 | 601 | qs@^6.14.0: 602 | version "6.14.0" 603 | resolved "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz" 604 | integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w== 605 | dependencies: 606 | side-channel "^1.1.0" 607 | 608 | range-parser@^1.2.1: 609 | version "1.2.1" 610 | resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" 611 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 612 | 613 | raw-body@^3.0.0: 614 | version "3.0.0" 615 | resolved "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz" 616 | integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== 617 | dependencies: 618 | bytes "3.1.2" 619 | http-errors "2.0.0" 620 | iconv-lite "0.6.3" 621 | unpipe "1.0.0" 622 | 623 | router@^2.2.0: 624 | version "2.2.0" 625 | resolved "https://registry.npmjs.org/router/-/router-2.2.0.tgz" 626 | integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ== 627 | dependencies: 628 | debug "^4.4.0" 629 | depd "^2.0.0" 630 | is-promise "^4.0.0" 631 | parseurl "^1.3.3" 632 | path-to-regexp "^8.0.0" 633 | 634 | safe-buffer@5.2.1: 635 | version "5.2.1" 636 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" 637 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 638 | 639 | "safer-buffer@>= 2.1.2 < 3.0.0": 640 | version "2.1.2" 641 | resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" 642 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 643 | 644 | send@^1.1.0, send@^1.2.0: 645 | version "1.2.0" 646 | resolved "https://registry.npmjs.org/send/-/send-1.2.0.tgz" 647 | integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw== 648 | dependencies: 649 | debug "^4.3.5" 650 | encodeurl "^2.0.0" 651 | escape-html "^1.0.3" 652 | etag "^1.8.1" 653 | fresh "^2.0.0" 654 | http-errors "^2.0.0" 655 | mime-types "^3.0.1" 656 | ms "^2.1.3" 657 | on-finished "^2.4.1" 658 | range-parser "^1.2.1" 659 | statuses "^2.0.1" 660 | 661 | serve-static@^2.2.0: 662 | version "2.2.0" 663 | resolved "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz" 664 | integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ== 665 | dependencies: 666 | encodeurl "^2.0.0" 667 | escape-html "^1.0.3" 668 | parseurl "^1.3.3" 669 | send "^1.2.0" 670 | 671 | setprototypeof@1.2.0: 672 | version "1.2.0" 673 | resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" 674 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 675 | 676 | shebang-command@^2.0.0: 677 | version "2.0.0" 678 | resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" 679 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 680 | dependencies: 681 | shebang-regex "^3.0.0" 682 | 683 | shebang-regex@^3.0.0: 684 | version "3.0.0" 685 | resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" 686 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 687 | 688 | side-channel-list@^1.0.0: 689 | version "1.0.0" 690 | resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz" 691 | integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== 692 | dependencies: 693 | es-errors "^1.3.0" 694 | object-inspect "^1.13.3" 695 | 696 | side-channel-map@^1.0.1: 697 | version "1.0.1" 698 | resolved "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz" 699 | integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== 700 | dependencies: 701 | call-bound "^1.0.2" 702 | es-errors "^1.3.0" 703 | get-intrinsic "^1.2.5" 704 | object-inspect "^1.13.3" 705 | 706 | side-channel-weakmap@^1.0.2: 707 | version "1.0.2" 708 | resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz" 709 | integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== 710 | dependencies: 711 | call-bound "^1.0.2" 712 | es-errors "^1.3.0" 713 | get-intrinsic "^1.2.5" 714 | object-inspect "^1.13.3" 715 | side-channel-map "^1.0.1" 716 | 717 | side-channel@^1.1.0: 718 | version "1.1.0" 719 | resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz" 720 | integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== 721 | dependencies: 722 | es-errors "^1.3.0" 723 | object-inspect "^1.13.3" 724 | side-channel-list "^1.0.0" 725 | side-channel-map "^1.0.1" 726 | side-channel-weakmap "^1.0.2" 727 | 728 | statuses@^2.0.1, statuses@2.0.1: 729 | version "2.0.1" 730 | resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" 731 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 732 | 733 | toidentifier@1.0.1: 734 | version "1.0.1" 735 | resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" 736 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 737 | 738 | type-is@^2.0.0, type-is@^2.0.1: 739 | version "2.0.1" 740 | resolved "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz" 741 | integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== 742 | dependencies: 743 | content-type "^1.0.5" 744 | media-typer "^1.1.0" 745 | mime-types "^3.0.0" 746 | 747 | undici-types@~6.21.0: 748 | version "6.21.0" 749 | resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz" 750 | integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== 751 | 752 | unpipe@1.0.0: 753 | version "1.0.0" 754 | resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" 755 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 756 | 757 | vary@^1, vary@^1.1.2: 758 | version "1.1.2" 759 | resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" 760 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 761 | 762 | web-streams-polyfill@^3.0.3: 763 | version "3.3.3" 764 | resolved "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz" 765 | integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== 766 | 767 | which@^2.0.1: 768 | version "2.0.2" 769 | resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" 770 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 771 | dependencies: 772 | isexe "^2.0.0" 773 | 774 | wrappy@1: 775 | version "1.0.2" 776 | resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" 777 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 778 | 779 | zod-to-json-schema@^3.24.1: 780 | version "3.24.5" 781 | resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz" 782 | integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g== 783 | 784 | zod@^3.23.8, zod@^3.24.1: 785 | version "3.24.2" 786 | resolved "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz" 787 | integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ== 788 | --------------------------------------------------------------------------------