Add structured runtime logging

This commit is contained in:
morluto 2026-07-12 10:57:34 +08:00
parent 5ed56d63e0
commit 03072e0ea7
16 changed files with 795 additions and 48 deletions

593
package-lock.json generated
View File

@ -11,6 +11,7 @@
"dependencies": {
"@modelcontextprotocol/server": "2.0.0-beta.3",
"incur": "0.4.13",
"pino": "^9.14.0",
"zod": "4.4.3"
},
"bin": {
@ -23,6 +24,7 @@
"husky": "9.1.7",
"jscpd": "5.0.12",
"knip": "6.26.0",
"lint-staged": "^16.4.0",
"oxlint": "1.73.0",
"prettier": "3.6.2",
"typedoc": "0.28.20",
@ -99,29 +101,6 @@
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
"license": "MIT"
},
"node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
@ -515,6 +494,29 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@oxc-parser/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@oxc-parser/binding-win32-arm64-msvc": {
"version": "0.137.0",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.137.0.tgz",
@ -1193,6 +1195,12 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@pinojs/redact": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
"license": "MIT"
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
@ -1416,6 +1424,29 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
"integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
"integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
@ -1580,6 +1611,7 @@
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@ -1597,6 +1629,7 @@
"integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.10",
@ -1735,6 +1768,48 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/ansi-escapes": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz",
"integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"environment": "^1.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -1764,6 +1839,15 @@
"js-tokens": "^10.0.0"
}
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@ -1774,6 +1858,56 @@
"node": ">=18"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"restore-cursor": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz",
"integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==",
"dev": true,
"license": "MIT",
"dependencies": {
"slice-ansi": "^8.0.0",
"string-width": "^8.2.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true,
"license": "MIT"
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@ -1806,6 +1940,13 @@
"node": ">=8"
}
},
"node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@ -1819,6 +1960,19 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/environment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz",
"integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/es-module-lexer": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.0.tgz",
@ -1836,6 +1990,13 @@
"@types/estree": "^1.0.0"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"dev": true,
"license": "MIT"
},
"node_modules/eventsource": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
@ -1928,6 +2089,19 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/get-east-asian-width": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-tsconfig": {
"version": "4.14.0",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
@ -1996,6 +2170,22 @@
"node": ">=22"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz",
"integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.3.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -2494,6 +2684,85 @@
"uc.micro": "^2.0.0"
}
},
"node_modules/lint-staged": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz",
"integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^14.0.3",
"listr2": "^9.0.5",
"picomatch": "^4.0.3",
"string-argv": "^0.3.2",
"tinyexec": "^1.0.4",
"yaml": "^2.8.2"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
},
"engines": {
"node": ">=20.17"
},
"funding": {
"url": "https://opencollective.com/lint-staged"
}
},
"node_modules/listr2": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz",
"integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"cli-truncate": "^5.0.0",
"colorette": "^2.0.20",
"eventemitter3": "^5.0.1",
"log-update": "^6.1.0",
"rfdc": "^1.4.1",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/log-update": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
"integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-escapes": "^7.0.0",
"cli-cursor": "^5.0.0",
"slice-ansi": "^7.1.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/slice-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz",
"integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"is-fullwidth-code-point": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
@ -2574,6 +2843,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nanoid": {
"version": "3.3.15",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
@ -2607,6 +2889,31 @@
"node": ">=12.20.0"
}
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/oxc-parser": {
"version": "0.137.0",
"resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.137.0.tgz",
@ -2772,6 +3079,43 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pino": {
"version": "9.14.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
"integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
"license": "MIT",
"dependencies": {
"@pinojs/redact": "^0.4.0",
"atomic-sleep": "^1.0.0",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^5.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-std-serializers": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
"license": "MIT"
},
"node_modules/pkce-challenge": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
@ -2827,6 +3171,22 @@
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
@ -2837,6 +3197,21 @@
"node": ">=6"
}
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/resolve-pkg-maps": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
@ -2847,6 +3222,30 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"dev": true,
"license": "MIT"
},
"node_modules/rolldown": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
@ -2881,6 +3280,15 @@
"@rolldown/binding-win32-x64-msvc": "1.1.5"
}
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
@ -2924,6 +3332,36 @@
"dev": true,
"license": "ISC"
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/slice-ansi": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
"integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.3",
"is-fullwidth-code-point": "^5.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/smol-toml": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.0.tgz",
@ -2937,6 +3375,15 @@
"url": "https://github.com/sponsors/cyyynthia"
}
},
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
"integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -2947,6 +3394,15 @@
"node": ">=0.10.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@ -2961,6 +3417,49 @@
"dev": true,
"license": "MIT"
},
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
"integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/string-width": {
"version": "8.2.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
"integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-json-comments": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
@ -2987,6 +3486,15 @@
"node": ">=8"
}
},
"node_modules/thread-stream": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz",
"integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@ -3114,6 +3622,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -3152,6 +3661,7 @@
"integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.5",
@ -3230,6 +3740,7 @@
"integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.1.10",
"@vitest/mocker": "4.1.10",
@ -3357,6 +3868,42 @@
"node": ">=8"
}
},
"node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yaml": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",

View File

@ -44,6 +44,7 @@
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"knip": "knip",
"lint:dead": "knip",
"jscpd": "jscpd src/ tests/",
"scan:todos": "node scripts/scan-todos.mjs",
"start": "node dist/main.js",
@ -60,9 +61,14 @@
"prepack": "npm run build",
"prepare": "husky"
},
"lint-staged": {
"*.{ts,mjs,json,md,yml,yaml}": "prettier --write",
"*.ts": "oxlint --fix"
},
"dependencies": {
"@modelcontextprotocol/server": "2.0.0-beta.3",
"incur": "0.4.13",
"pino": "^9.14.0",
"zod": "4.4.3"
},
"devDependencies": {
@ -72,6 +78,7 @@
"husky": "9.1.7",
"jscpd": "5.0.12",
"knip": "6.26.0",
"lint-staged": "^16.4.0",
"oxlint": "1.73.0",
"prettier": "3.6.2",
"typedoc": "0.28.20",

View File

@ -1,6 +1,7 @@
import { parseConfig } from "../config.js";
import type { JsonValue } from "../hopper/protocol.js";
import { createBinarySession } from "./runtime.js";
import { silentLogger, type Logger } from "../logger.js";
/**
* Open one binary, execute one tool, and always release the bridge session.
@ -11,11 +12,12 @@ export const runDirectAnalysis = async (
path: string,
tool: "binary_overview" | "procedure_pseudo_code",
arguments_: Readonly<Record<string, JsonValue>>,
logger: Logger = silentLogger,
): Promise<JsonValue> => {
const config = parseConfig(process.env);
if (!config.ok)
return { error: config.error._tag, message: config.error.message };
const session = createBinarySession(config.value);
const session = createBinarySession(config.value, logger);
try {
const opened = await session.open(path);
if (!opened.ok)

View File

@ -4,13 +4,17 @@ import type { AppConfig } from "../config.js";
import { BinarySession } from "./BinarySession.js";
import { HopperApplicationLauncher } from "../hopper/BridgeLauncher.js";
import { HopperClient } from "../hopper/HopperClient.js";
import { silentLogger, type Logger } from "../logger.js";
/**
* Compose the target-switching runtime shared directly by CLI and MCP adapters.
* This is the sole production wiring point for Hopper so neither adapter shells
* out to the other and both retain identical loader and bridge semantics.
*/
export const createBinarySession = (config: AppConfig): BinarySession => {
export const createBinarySession = (
config: AppConfig,
logger: Logger = silentLogger,
): BinarySession => {
const bridgeScriptPath = fileURLToPath(
new URL("../../bridge/hopper_bridge.py", import.meta.url),
);
@ -27,6 +31,7 @@ export const createBinarySession = (config: AppConfig): BinarySession => {
: target.loaderArgs,
bridgeScriptPath,
}),
logger: logger.child({ layer: "bridge" }),
}),
);
};

View File

@ -5,6 +5,7 @@ import { runDoctor } from "./application/Doctor.js";
import { runDirectAnalysis } from "./application/DirectAnalysis.js";
import { runSetup } from "./application/Setup.js";
import { PRODUCT_IDENTITY } from "./identity.js";
import { createLogger, parseLogLevel, type Logger } from "./logger.js";
/**
* Build the one-shot Incur CLI without starting Hopper at import time.
@ -12,6 +13,12 @@ import { PRODUCT_IDENTITY } from "./identity.js";
* `--mcp` are intercepted by the executable dispatcher before this module loads.
*/
export const createCli = (): ReturnType<typeof Cli.create> => {
const logger = createLogger(
"cli",
process.env.REA_LOG_LEVEL === undefined
? "silent"
: parseLogLevel(process.env.REA_LOG_LEVEL),
);
const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
version: "0.1.0",
description: "Install, diagnose, and automate Hopper binary analysis.",
@ -40,21 +47,26 @@ export const createCli = (): ReturnType<typeof Cli.create> => {
.describe("Approve prerequisite installation"),
}),
alias: { yes: "y" },
run: ({ options }) => runSetup(options.yes),
run: ({ options }) =>
logCliCommand(logger, "setup", () => runSetup(options.yes)),
});
cli.command("doctor", {
description: "Diagnose the local Hopper integration",
options: z.object({
target: z.string().optional().describe("Optional binary path to check"),
}),
run: ({ options }) => runDoctor(options.target),
run: ({ options }) =>
logCliCommand(logger, "doctor", () => runDoctor(options.target)),
});
cli.command("analyze", {
description: "Analyze a binary through the shared Hopper core",
args: z.object({
path: z.string().describe("Binary or Hopper database path"),
}),
run: ({ args }) => runDirectAnalysis(args.path, "binary_overview", {}),
run: ({ args }) =>
logCliCommand(logger, "analyze", () =>
runDirectAnalysis(args.path, "binary_overview", {}, logger),
),
});
cli.command("decompile", {
description: "Decompile an address through the shared Hopper core",
@ -63,9 +75,44 @@ export const createCli = (): ReturnType<typeof Cli.create> => {
address: z.string().describe("Procedure address"),
}),
run: ({ args }) =>
runDirectAnalysis(args.path, "procedure_pseudo_code", {
procedure: args.address,
}),
logCliCommand(logger, "decompile", () =>
runDirectAnalysis(
args.path,
"procedure_pseudo_code",
{ procedure: args.address },
logger,
),
),
});
return cli;
};
const logCliCommand = async <Value>(
logger: Logger,
command: string,
execute: () => Promise<Value>,
): Promise<Value> => {
const startedAt = performance.now();
try {
const value = await execute();
logger.info(
{
command,
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
status: "ok",
},
"CLI command completed",
);
return value;
} catch (cause: unknown) {
logger.error(
{
command,
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
status: "error",
},
"CLI command failed",
);
throw cause;
}
};

View File

@ -2,6 +2,7 @@ import { z } from "zod";
import { ConfigurationError } from "./domain/errors.js";
import { err, ok, type Result } from "./domain/result.js";
import type { LogLevel } from "./logger.js";
const DEFAULT_HOPPER_LAUNCHER_PATH =
"/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
@ -11,6 +12,7 @@ export interface AppConfig {
readonly hopperTargetPath: string | undefined;
readonly hopperTargetKind: "executable" | "database";
readonly hopperLoaderArgs: readonly string[];
readonly logLevel: LogLevel;
}
const environmentSchema = z.object({
@ -18,6 +20,9 @@ const environmentSchema = z.object({
HOPPER_TARGET_PATH: z.string().min(1).optional(),
HOPPER_TARGET_KIND: z.enum(["executable", "database"]).default("executable"),
HOPPER_LOADER_ARGS_JSON: z.string().optional(),
REA_LOG_LEVEL: z
.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
.default("info"),
});
/**
@ -66,5 +71,6 @@ export const parseConfig = (
hopperTargetPath: parsedEnvironment.data.HOPPER_TARGET_PATH,
hopperTargetKind: parsedEnvironment.data.HOPPER_TARGET_KIND,
hopperLoaderArgs: parsedArgs.data,
logLevel: parsedEnvironment.data.REA_LOG_LEVEL,
});
};

View File

@ -14,6 +14,7 @@ import {
} from "../domain/errors.js";
import { err, ok, type Result } from "../domain/result.js";
import type { BridgeLaunch, BridgeLauncher } from "./BridgeLauncher.js";
import { silentLogger, type Logger } from "../logger.js";
import {
parseResponseLine,
responseResult,
@ -29,6 +30,7 @@ export interface HopperClientOptions {
readonly requestTimeoutMs?: number;
readonly startupTimeoutMs?: number;
readonly onDiagnostic?: (event: HopperDiagnostic) => void;
readonly logger?: Logger;
}
/** Safe launcher telemetry; stderr content is intentionally never exposed. */
@ -62,6 +64,7 @@ export class HopperClient {
> &
HopperClientOptions;
readonly #pending = new Map<number, PendingRequest>();
readonly #logger: Logger;
#socket: Socket | undefined;
#launch: BridgeLaunch | undefined;
#directory: string | undefined;
@ -73,6 +76,7 @@ export class HopperClient {
#closePromise: Promise<void> | undefined;
constructor(options: HopperClientOptions) {
this.#logger = options.logger ?? silentLogger;
this.#options = {
...options,
requestTimeoutMs: options.requestTimeoutMs ?? 30_000,
@ -224,6 +228,7 @@ export class HopperClient {
if (options.signal?.aborted === true)
return err(new HopperCancelledError());
const id = this.#nextId++;
const startedAt = performance.now();
const timeoutMs = options.timeoutMs ?? this.#options.requestTimeoutMs;
const response = new Promise<Result<JsonValue, HopperError>>((resolve) => {
const onAbort =
@ -252,7 +257,17 @@ export class HopperClient {
this.#settle(id, err(new HopperProcessError(null)));
},
);
return response;
const result = await response;
this.#logger[result.ok ? "debug" : "warn"](
{
method,
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
status: result.ok ? "ok" : "error",
...(result.ok ? {} : { errorTag: result.error._tag }),
},
"Hopper bridge request completed",
);
return result;
}
#attachSocket(socket: Socket): void {

41
src/logger.ts Normal file
View File

@ -0,0 +1,41 @@
import pino, { type Logger } from "pino";
import { z } from "zod";
/** Supported structured log levels at the process boundary. */
export type LogLevel = z.infer<typeof logLevelSchema>;
const logLevelSchema = z
.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
.default("info");
/** Parse the optional REA_LOG_LEVEL environment variable. */
export const parseLogLevel = (value: unknown): LogLevel =>
logLevelSchema.parse(value);
/** Create a JSON logger whose destination cannot corrupt MCP stdout frames. */
export const createLogger = (mode: "mcp" | "cli", level: LogLevel): Logger =>
pino(
{
level,
base: { application: "rea", mode },
redact: {
paths: [
"token",
"*.token",
"params",
"*.params",
"arguments",
"*.arguments",
"environment",
"*.environment",
],
censor: "[Redacted]",
},
},
pino.destination({ dest: mode === "mcp" ? 2 : 1, sync: false }),
);
/** Logger used by embedders that have not opted into process output. */
export const silentLogger = pino({ level: "silent" });
export type { Logger };

View File

@ -11,6 +11,7 @@ import {
import { parseConfig } from "./config.js";
import { createBinarySession } from "./application/runtime.js";
import { createServer } from "./server/createServer.js";
import { createLogger } from "./logger.js";
/**
* Start the long-lived MCP adapter and install idempotent shutdown handlers.
@ -23,27 +24,35 @@ export const run = async (): Promise<number> => {
process.stderr.write(`${config.error._tag}: ${config.error.message}\n`);
return 1;
}
const logger = createLogger("mcp", config.value.logLevel);
const serverLogger = logger.child({ layer: "server" });
const session = createBinarySession(config.value);
const session = createBinarySession(config.value, logger);
if (config.value.hopperTargetPath !== undefined) {
const opened = await session.open(config.value.hopperTargetPath, {
targetKind: config.value.hopperTargetKind,
});
if (!opened.ok) {
await session.close();
serverLogger.error(
{ errorTag: opened.error._tag },
"Initial target failed to open",
);
process.stderr.write(`${opened.error._tag}: ${opened.error.message}\n`);
return 1;
}
}
let handle: StdioServerHandle;
try {
handle = serveStdio(() => createServer(session, session), {
handle = serveStdio(() => createServer(session, session, logger), {
onerror: () => {
serverLogger.error("MCP stdio transport error");
process.stderr.write("MCP stdio transport error\n");
},
});
} catch {
await session.close();
serverLogger.error("Failed to start MCP stdio transport");
process.stderr.write("Failed to start MCP stdio transport\n");
return 1;
}
@ -59,6 +68,7 @@ export const run = async (): Promise<number> => {
const requestShutdown = (): void => {
shutdown().catch(() => {
process.exitCode = 1;
serverLogger.error("Failed to shut down cleanly");
process.stderr.write("Failed to shut down cleanly\n");
});
};

View File

@ -6,6 +6,7 @@ import { PRODUCT_IDENTITY } from "../identity.js";
import { registerEnhancedTools } from "./registerEnhancedTools.js";
import { registerOfficialTools } from "./registerOfficialTools.js";
import { registerSessionTools } from "./registerSessionTools.js";
import { silentLogger, type Logger } from "../logger.js";
/**
* Construct one MCP server without acquiring subprocess resources.
@ -15,6 +16,7 @@ import { registerSessionTools } from "./registerSessionTools.js";
export const createServer = (
hopper: HopperToolPort,
session?: BinarySession,
logger: Logger = silentLogger,
): McpServer => {
const server = new McpServer(
{ name: PRODUCT_IDENTITY.mcpServerKey, version: "0.1.0" },
@ -26,8 +28,9 @@ export const createServer = (
: "Reverse-engineering tools for Hopper Disassembler. Open a target with open_binary, then start with binary_overview.",
},
);
registerOfficialTools(server, hopper);
registerEnhancedTools(server, hopper);
if (session !== undefined) registerSessionTools(server, session);
const toolLogger = logger.child({ layer: "server" });
registerOfficialTools(server, hopper, toolLogger);
registerEnhancedTools(server, hopper, toolLogger);
if (session !== undefined) registerSessionTools(server, session, toolLogger);
return server;
};

View File

@ -8,15 +8,18 @@ import {
type ToolContract,
} from "../contracts/toolContracts.js";
import { toCallToolResult } from "./toolResult.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
/** Register composed workflows against the same port as direct bridge tools. */
export const registerEnhancedTools = (
server: McpServer,
hopper: HopperToolPort,
logger: Logger,
): void => {
const services = new EnhancedTools(hopper);
for (const contract of ENHANCED_TOOL_CONTRACTS) {
registerEnhancedTool(server, services, contract);
registerEnhancedTool(server, services, contract, logger);
}
};
@ -24,6 +27,7 @@ const registerEnhancedTool = (
server: McpServer,
services: EnhancedTools,
contract: ToolContract,
logger: Logger,
): void => {
const name = enhancedToolNameSchema.parse(contract.name);
server.registerTool(
@ -31,7 +35,9 @@ const registerEnhancedTool = (
{ description: contract.description, inputSchema: contract.inputSchema },
async (input, context) =>
toCallToolResult(
await services.execute(name, input, context.mcpReq.signal),
await logToolExecution(logger, name, () =>
services.execute(name, input, context.mcpReq.signal),
),
),
);
};

View File

@ -7,14 +7,17 @@ import {
} from "../contracts/toolContracts.js";
import { jsonValueSchema, type JsonValue } from "../hopper/protocol.js";
import { toCallToolResult } from "./toolResult.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
/** Register direct bridge proxies, preserving MCP cancellation and typed errors. */
export const registerOfficialTools = (
server: McpServer,
hopper: HopperToolPort,
logger: Logger,
): void => {
for (const contract of OFFICIAL_TOOL_CONTRACTS) {
registerOfficialTool(server, hopper, contract);
registerOfficialTool(server, hopper, contract, logger);
}
};
@ -22,6 +25,7 @@ const registerOfficialTool = (
server: McpServer,
hopper: HopperToolPort,
contract: ToolContract,
logger: Logger,
): void => {
server.registerTool(
contract.name,
@ -31,9 +35,11 @@ const registerOfficialTool = (
},
async (input, context) => {
const arguments_ = projectOfficialArguments(contract, input);
const result = await hopper.callTool(contract.name, arguments_, {
signal: context.mcpReq.signal,
});
const result = await logToolExecution(logger, contract.name, () =>
hopper.callTool(contract.name, arguments_, {
signal: context.mcpReq.signal,
}),
);
return toCallToolResult(result);
},
);

View File

@ -4,11 +4,14 @@ import { z } from "zod";
import type { BinarySession } from "../application/BinarySession.js";
import { SESSION_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import { toCallToolResult } from "./toolResult.js";
import type { Logger } from "../logger.js";
import { logToolExecution } from "./toolLogging.js";
/** Register MCP-only target lifecycle operations on a long-lived session. */
export const registerSessionTools = (
server: McpServer,
session: BinarySession,
logger: Logger,
): void => {
const [openContract, closeContract, statusContract] = SESSION_TOOL_CONTRACTS;
server.registerTool(
@ -19,9 +22,9 @@ export const registerSessionTools = (
},
async (input, context) => {
const parsed = z.object({ path: z.string().min(1) }).parse(input);
const opened = await session.open(parsed.path, {
signal: context.mcpReq.signal,
});
const opened = await logToolExecution(logger, openContract.name, () =>
session.open(parsed.path, { signal: context.mcpReq.signal }),
);
return opened.ok
? toCallToolResult({
ok: true,
@ -41,7 +44,12 @@ export const registerSessionTools = (
description: closeContract.description,
inputSchema: closeContract.inputSchema,
},
async () => toCallToolResult(await session.close()),
async () =>
toCallToolResult(
await logToolExecution(logger, closeContract.name, () =>
session.close(),
),
),
);
server.registerTool(
statusContract.name,

21
src/server/toolLogging.ts Normal file
View File

@ -0,0 +1,21 @@
import type { Result } from "../domain/result.js";
import type { Logger } from "../logger.js";
/** Log one MCP tool's duration and status without recording caller input. */
export const logToolExecution = async <Value, Failure>(
logger: Logger,
tool: string,
execute: () => Promise<Result<Value, Failure>>,
): Promise<Result<Value, Failure>> => {
const startedAt = performance.now();
const result = await execute();
logger[result.ok ? "info" : "warn"](
{
tool,
durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
status: result.ok ? "ok" : "error",
},
"MCP tool execution completed",
);
return result;
};

View File

@ -13,6 +13,7 @@ describe("runtime configuration", () => {
expect(result.value.hopperLauncherPath).toContain("/MacOS/hopper");
expect(result.value.hopperTargetKind).toBe("executable");
expect(result.value.hopperLoaderArgs).toEqual([]);
expect(result.value.logLevel).toBe("info");
}
});
@ -31,6 +32,7 @@ describe("runtime configuration", () => {
hopperTargetPath: "/fixture/sample.hop",
hopperTargetKind: "database",
hopperLoaderArgs: ["-l", "FAT", "--aarch64", "-l", "Mach-O"],
logLevel: "info",
},
});
});
@ -46,4 +48,10 @@ describe("runtime configuration", () => {
).toBe(false);
},
);
it("parses supported log levels and rejects unknown levels", () => {
const configured = parseConfig({ REA_LOG_LEVEL: "debug" });
expect(configured.ok && configured.value.logLevel).toBe("debug");
expect(parseConfig({ REA_LOG_LEVEL: "verbose" }).ok).toBe(false);
});
});

View File

@ -42,7 +42,22 @@ describe("production stdio runtime", () => {
await client.close();
await transport.close();
}
expect(stderr).toBe("");
const records = stderr
.trim()
.split("\n")
.filter((line) => line.length > 0)
.map((line: string): unknown => JSON.parse(line));
expect(records).toContainEqual(
expect.objectContaining({
application: "rea",
mode: "mcp",
layer: "server",
tool: "current_document",
status: "ok",
}),
);
expect(stderr).not.toContain("HOPPER_LOADER_ARGS_JSON");
expect(stderr).not.toContain(fixturePath);
});
it("honors the configured kind for an initial database target", async () => {