From 51029c7981dffeee0794443db9b82ca2a05aba5b Mon Sep 17 00:00:00 2001 From: vreshch Date: Mon, 14 Sep 2026 00:20:55 +0200 Subject: [PATCH 1/2] feat: optional observability via kit bootstrap The stdio bin tries `@agentage/observability/bootstrap` behind a try/catch before loading the MCP SDK, so an installed kit instruments tool registration and absent means a silent skip. The kit stays out of `dependencies` - this package is run via `npx` and the kit would add ~41 packages to every cold start. --- README.md | 16 ++ package-lock.json | 564 +++++++++++++++++++++++++++++++++++++ package.json | 2 + src/bin/server-memory.ts | 28 +- test/observability.test.ts | 146 ++++++++++ 5 files changed, 752 insertions(+), 4 deletions(-) create mode 100644 test/observability.test.ts diff --git a/README.md b/README.md index eb71793..5092150 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,22 @@ import { createMemoryServer, loadLocalServer } from '@agentage/server-memory'; // await server.connect(new StreamableHTTPServerTransport(...)); ``` +## Telemetry (optional, off by default) + +Nothing is collected and nothing extra is installed: `@agentage/observability` is **not** a +dependency, because `npx @agentage/server-memory` should stay a small cold start. Install the kit +alongside and the bin picks it up on its own - traces and one wide event per tool call: + +```bash +npm i -g @agentage/observability # or just use the agentage CLI, which ships with it +``` + +Absent, the server behaves exactly as before (`AGENTAGE_DEBUG=1` prints one stderr line saying the +kit was not found). Present, it is still inert until `OTEL_EXPORTER_OTLP_ENDPOINT` names a +collector; `OTEL_SERVICE_NAME` defaults to `agentage-server-memory`. Either way **stdout carries +only JSON-RPC** - every kit line goes to stderr, which `test/observability.test.ts` asserts in both +directions. + ## Develop ```bash diff --git a/package-lock.json b/package-lock.json index 9b62eaf..531dc3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,8 @@ "agentage-server-memory": "dist/bin/server-memory.js" }, "devDependencies": { + "@agentage/observability": "^1.0.0", + "@opentelemetry/api": "^1.9.1", "@types/node": "^26.1.2", "@typescript-eslint/eslint-plugin": "^8.18.0", "@typescript-eslint/parser": "^8.18.0", @@ -47,6 +49,61 @@ "npm": ">=10.0.0" } }, + "node_modules/@agentage/observability": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@agentage/observability/-/observability-1.0.0.tgz", + "integrity": "sha512-TbHSd4QGx5dky0x5bsabFWC955a2vTxafEcv3IYe1ppiYY4/JKcqSQCeoguvfwLlySsO8pN0ODvcz3yet6eibA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@opentelemetry/exporter-trace-otlp-http": "^0.222.0", + "@opentelemetry/instrumentation": "^0.222.0", + "@opentelemetry/instrumentation-express": "^0.70.0", + "@opentelemetry/instrumentation-http": "^0.222.0", + "@opentelemetry/instrumentation-undici": "^0.32.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-trace-base": "^2.2.0", + "@opentelemetry/sdk-trace-node": "^2.11.0", + "@opentelemetry/semantic-conventions": "^1.43.0", + "pino": "^10.3.1" + }, + "engines": { + "node": ">=22.13" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": ">=1.11 <2", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/instrumentation-amqplib": "^0.69.0", + "@opentelemetry/instrumentation-mongodb": "^0.75.0", + "@opentelemetry/instrumentation-pg": "^0.74.0", + "@opentelemetry/instrumentation-redis": "^0.70.0", + "@vercel/otel": ">=2", + "react": ">=18" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + }, + "@opentelemetry/instrumentation-amqplib": { + "optional": true + }, + "@opentelemetry/instrumentation-mongodb": { + "optional": true + }, + "@opentelemetry/instrumentation-pg": { + "optional": true + }, + "@opentelemetry/instrumentation-redis": { + "optional": true + }, + "@vercel/otel": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -395,6 +452,305 @@ } } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", + "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.11.0.tgz", + "integrity": "sha512-Tr79DyWI8itsBdg+jH+opjfrwLzX+erk1/ExkIwhWoAVjVrJIn2y5+cGjTC0Vy8fyNIA/y8wuJPZwr1T3xCZeQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.222.0.tgz", + "integrity": "sha512-RCnPWcHppwiquQ+cV3nWvNwdf0MG1w26e5jewW2T83nTZOlXgcg88sY9ulCVgagGHcq1mj0L0GP6YHgzk2v8oA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/sdk-trace": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.222.0.tgz", + "integrity": "sha512-fhbRDuAgzPKpq1k5+hX3uS+3QAYNca0on0Ngot/R8dDsQtuFaADeZnG2uSgxABuvcI7S2phAaRhbct9jieeKZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.70.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.70.0.tgz", + "integrity": "sha512-OHdj/2apRZNhMKkd6w4OV4SnX/aS2LWMLyHwGGNYhWratgh88o+cVJS9FvqC+HCYWSDWTEWEfl/8d7BdEI7c+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.9.0", + "@opentelemetry/instrumentation": "^0.222.0", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.222.0.tgz", + "integrity": "sha512-HhMHW32LyGzIJEmUElxaML4oUe6JiCq0Hvjt/Awxb60SlMsSFetFLFjed45g/0wrPr1n7z+xW+x1B/584WBAkg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/instrumentation": "0.222.0", + "@opentelemetry/semantic-conventions": "^1.29.0", + "forwarded-parse": "2.1.2" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.32.0.tgz", + "integrity": "sha512-Ms8oPkrCHa6hJOYodcFC8QOt1AFlns3KD1CqXBEihXyTFeLXDO84tIXYyKlXNHr8jMrl1Gm2velDvsSDXAuh9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^2.9.0", + "@opentelemetry/instrumentation": "^0.222.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.222.0.tgz", + "integrity": "sha512-YbywG3veEm2Fb6TbdxRkuquWob6eVWXuA8/Ba1tXz9jHfUqpdE3keilOHEtPboC4CvS1bjeeVfNkWGOOrLj+lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/otlp-transformer": "0.222.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.222.0.tgz", + "integrity": "sha512-/F3BZ89+CJQnZkMh2tCrtcdB+XT2Dxhj4FFE+WPQ//413hmFL0/RfEX6vgOIWGhiSzrkHWTK3+6SiT7K5/g/jQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-logs": "0.222.0", + "@opentelemetry/sdk-metrics": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.222.0.tgz", + "integrity": "sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.11.0.tgz", + "integrity": "sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", + "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz", + "integrity": "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-node": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.11.0.tgz", + "integrity": "sha512-CuvCMJmZxswhNLlM2LfuLOW3h3fZujA4hsG4B+Sz4dX2zvaXO8Ng74cnDHWD64gLszTlhiG3c0iNUjj4g+0/sA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/context-async-hooks": "2.11.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/sdk-trace-base": "2.11.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@oxc-project/types": { "version": "0.148.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.148.0.tgz", @@ -406,6 +762,13 @@ "url": "https://github.com/sponsors/oxc-project" } }, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/@pkgr/core": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", @@ -1160,6 +1523,16 @@ "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1282,6 +1655,13 @@ "node": ">=18" } }, + "node_modules/cjs-module-lexer": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", + "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -1992,6 +2372,13 @@ "node": ">= 0.6" } }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "dev": true, + "license": "MIT" + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -2187,6 +2574,28 @@ "node": ">= 4" } }, + "node_modules/import-in-the-middle": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.1.tgz", + "integrity": "sha512-mPKuL8bPQzecui2KK6Gb+M8JvJoHnhS1FeYGa22QopBmlevF5F0FE6ued/B5EgHDeIoMTONIpDWWFKUPOG0DBQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.2", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/import-in-the-middle/node_modules/es-module-lexer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.2.tgz", + "integrity": "sha512-BuIB67FngDSyQ/dpQNOZybwdEBDUGJQvOqwWr4ha/ufYiqzuEwPkKO2zLhRAgay28tStRIHUeWmszZAJo3GCOg==", + "dev": true, + "license": "MIT" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -2697,6 +3106,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2774,6 +3190,16 @@ "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2904,6 +3330,46 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.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": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -2982,6 +3448,23 @@ "node": ">=6.0.0" } }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -3040,6 +3523,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3064,6 +3554,16 @@ "node": ">= 0.10" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3073,6 +3573,20 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/rolldown": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.7.tgz", @@ -3124,6 +3638,16 @@ "node": ">= 18" } }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -3294,6 +3818,16 @@ "dev": true, "license": "ISC" }, + "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==", + "dev": true, + "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", @@ -3304,6 +3838,16 @@ "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -3343,6 +3887,26 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "dev": true, + "license": "MIT" + }, "node_modules/tinybench": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.1.4.tgz", diff --git a/package.json b/package.json index 25ec116..10bc5bb 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,8 @@ "zod": "^4.4.3" }, "devDependencies": { + "@agentage/observability": "^1.0.0", + "@opentelemetry/api": "^1.9.1", "@types/node": "^26.1.2", "@typescript-eslint/eslint-plugin": "^8.18.0", "@typescript-eslint/parser": "^8.18.0", diff --git a/src/bin/server-memory.ts b/src/bin/server-memory.ts index fe1a167..57263be 100644 --- a/src/bin/server-memory.ts +++ b/src/bin/server-memory.ts @@ -7,8 +7,29 @@ // // stdout is the JSON-RPC wire; all diagnostics MUST go to stderr. -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { loadLocalServer } from '../server/local-server.js'; +const reason = (err: unknown): string => (err instanceof Error ? err.message : String(err)); + +// Optional observability. @agentage/observability is deliberately NOT a dependency: this package is +// run through `npx`, and the kit would triple a cold start's install. Installed alongside (global, +// or via the agentage CLI) its bootstrap adds tool spans, wide events and crash capture; absent, it +// is a silent skip. Either way stdout stays the JSON-RPC wire - the kit logs to stderr only, and +// stays inert until OTEL_EXPORTER_OTLP_ENDPOINT names a collector. +const startObservability = async (): Promise => { + try { + if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = 'agentage-server-memory'; + await import('@agentage/observability/bootstrap'); + } catch (err) { + if (process.env.AGENTAGE_DEBUG) { + process.stderr.write(`[server-memory] observability off: ${reason(err)}\n`); + } + } +}; + +// Loaded behind the bootstrap, not at the top: the kit instruments MCP tool registration through +// module hooks, which only see a module imported after they are registered. +await startObservability(); +const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js'); +const { loadLocalServer } = await import('../server/local-server.js'); const main = async (): Promise => { const server = await loadLocalServer(); @@ -17,7 +38,6 @@ const main = async (): Promise => { }; main().catch((err: unknown) => { - const message = err instanceof Error ? err.message : String(err); - process.stderr.write(`[server-memory] fatal: ${message}\n`); + process.stderr.write(`[server-memory] fatal: ${reason(err)}\n`); process.exit(1); }); diff --git a/test/observability.test.ts b/test/observability.test.ts new file mode 100644 index 0000000..771462d --- /dev/null +++ b/test/observability.test.ts @@ -0,0 +1,146 @@ +import { execFileSync, spawn } from 'node:child_process'; +import { + cpSync, + mkdirSync, + mkdtempSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { init } from '@agentage/memory-core'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +// The regression this file exists to prevent: stdout is the JSON-RPC wire. Whether the optional +// @agentage/observability kit is installed alongside or not, nothing but JSON-RPC may reach it - +// the kit's own lines belong on stderr. +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); +const bin = join(repoRoot, 'dist/bin/server-memory.js'); +const tmps: string[] = []; +const mk = (p: string): string => { + const d = mkdtempSync(join(tmpdir(), p)); + tmps.push(d); + return d; +}; + +interface Run { + stdout: string; + stderr: string; +} + +// Drive one initialize + memory__list over raw stdio and capture both streams verbatim. +const runBin = async ( + binPath: string, + configDir: string, + env: NodeJS.ProcessEnv = {} +): Promise => { + const child = spawn(process.execPath, [binPath], { + env: { ...process.env, ...env, AGENTAGE_CONFIG_DIR: configDir }, + stdio: ['pipe', 'pipe', 'pipe'], + }); + const run: Run = { stdout: '', stderr: '' }; + child.stdout.on('data', (d: Buffer) => (run.stdout += d.toString())); + child.stderr.on('data', (d: Buffer) => (run.stderr += d.toString())); + const send = (msg: unknown): void => void child.stdin.write(`${JSON.stringify(msg)}\n`); + send({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2025-06-18', + capabilities: {}, + clientInfo: { name: 't', version: '0' }, + }, + }); + await new Promise((r) => setTimeout(r, 400)); + send({ jsonrpc: '2.0', method: 'notifications/initialized' }); + send({ + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { name: 'memory__list', arguments: {} }, + }); + await new Promise((r) => setTimeout(r, 1200)); + child.kill(); + return run; +}; + +const jsonRpcOnly = (stdout: string): boolean => + stdout + .split('\n') + .filter(Boolean) + .every((line) => { + try { + return (JSON.parse(line) as { jsonrpc?: string }).jsonrpc === '2.0'; + } catch { + return false; + } + }); + +// A copy of dist beside a node_modules holding every runtime dependency but NOT the kit: exactly +// what a `npx @agentage/server-memory` cold start looks like. +const withoutKit = (): string => { + const root = mk('sm-nokit-'); + cpSync(join(repoRoot, 'dist'), join(root, 'dist'), { recursive: true }); + writeFileSync(join(root, 'package.json'), '{"type":"module"}'); + const deps = Object.keys( + ( + JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf-8')) as { + dependencies: Record; + } + ).dependencies + ); + for (const dep of deps) { + const target = join(root, 'node_modules', dep); + mkdirSync(dirname(target), { recursive: true }); + symlinkSync(join(repoRoot, 'node_modules', dep), target); + } + return join(root, 'dist/bin/server-memory.js'); +}; + +describe('optional observability never touches stdout', () => { + let configDir: string; + + beforeAll(async () => { + execFileSync('npm', ['run', 'build'], { cwd: repoRoot, stdio: 'ignore' }); + configDir = mk('sm-obs-cfg-'); + await init({ configDir, vaultName: 'work', vaultPath: mk('sm-obs-vault-') }); + }, 60_000); + + afterAll(() => { + while (tmps.length) rmSync(tmps.pop()!, { recursive: true, force: true }); + }); + + it('with the kit installed: JSON-RPC on stdout, the tool event on stderr', async () => { + const run = await runBin(bin, configDir); + expect(jsonRpcOnly(run.stdout)).toBe(true); + const tool = run.stderr + .split('\n') + .filter(Boolean) + .map((l) => JSON.parse(l) as { kind?: string; tool?: string; service?: string }); + expect(tool).toContainEqual( + expect.objectContaining({ + kind: 'tool', + tool: 'memory__list', + service: 'agentage-server-memory', + }) + ); + }, 30_000); + + it('without the kit installed: JSON-RPC on stdout, a silent skip on stderr', async () => { + const run = await runBin(withoutKit(), configDir); + expect(jsonRpcOnly(run.stdout)).toBe(true); + // initialize + the tool call both answered, so the server really served without the kit + expect(run.stdout.split('\n').filter(Boolean)).toHaveLength(2); + expect(run.stderr).toBe(''); + }, 30_000); + + it('without the kit installed: AGENTAGE_DEBUG explains the skip, on stderr', async () => { + const run = await runBin(withoutKit(), configDir, { AGENTAGE_DEBUG: '1' }); + expect(jsonRpcOnly(run.stdout)).toBe(true); + expect(run.stderr).toContain('[server-memory] observability off:'); + }, 30_000); +}); From 2cb4604b8b9348f384ae991644b29b5161494deb Mon Sep 17 00:00:00 2001 From: vreshch Date: Mon, 14 Sep 2026 00:27:05 +0200 Subject: [PATCH 2/2] fix: keep the kit's tracer banner off the JSON-RPC wire The kit announces an enabled tracer with a console.log, so a user who sets OTEL_EXPORTER_OTLP_ENDPOINT would get a non-JSON-RPC frame mid-handshake. stdout is diverted to stderr for the duration of the bootstrap import. --- src/bin/server-memory.ts | 7 +++++++ test/observability.test.ts | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/bin/server-memory.ts b/src/bin/server-memory.ts index 57263be..6cecdbf 100644 --- a/src/bin/server-memory.ts +++ b/src/bin/server-memory.ts @@ -15,6 +15,11 @@ const reason = (err: unknown): string => (err instanceof Error ? err.message : S // is a silent skip. Either way stdout stays the JSON-RPC wire - the kit logs to stderr only, and // stays inert until OTEL_EXPORTER_OTLP_ENDPOINT names a collector. const startObservability = async (): Promise => { + // The kit (1.0.0) announces an enabled tracer with one console.log, which on this process would + // land mid-handshake on the JSON-RPC wire. Divert stdout to stderr while it boots; drop this once + // the kit writes that line to stderr itself. + const stdoutWrite = process.stdout.write; + process.stdout.write = process.stderr.write.bind(process.stderr) as typeof process.stdout.write; try { if (!process.env.OTEL_SERVICE_NAME) process.env.OTEL_SERVICE_NAME = 'agentage-server-memory'; await import('@agentage/observability/bootstrap'); @@ -22,6 +27,8 @@ const startObservability = async (): Promise => { if (process.env.AGENTAGE_DEBUG) { process.stderr.write(`[server-memory] observability off: ${reason(err)}\n`); } + } finally { + process.stdout.write = stdoutWrite; } }; diff --git a/test/observability.test.ts b/test/observability.test.ts index 771462d..b4af4b1 100644 --- a/test/observability.test.ts +++ b/test/observability.test.ts @@ -130,6 +130,14 @@ describe('optional observability never touches stdout', () => { ); }, 30_000); + // The kit announces an enabled tracer on stdout (console.log, kit 1.0.0); on this process that + // would be a non-JSON-RPC frame mid-handshake, so the bin diverts stdout while it boots. + it('with a collector configured: the tracer banner lands on stderr, not the wire', async () => { + const run = await runBin(bin, configDir, { OTEL_EXPORTER_OTLP_ENDPOINT: 'http://127.0.0.1:9' }); + expect(jsonRpcOnly(run.stdout)).toBe(true); + expect(run.stderr).toContain('otel: tracing enabled'); + }, 30_000); + it('without the kit installed: JSON-RPC on stdout, a silent skip on stderr', async () => { const run = await runBin(withoutKit(), configDir); expect(jsonRpcOnly(run.stdout)).toBe(true);