From df9dddabea0bf6ab6a9d1ad2ef58e1e0c50b2f6a Mon Sep 17 00:00:00 2001 From: itz4blitz Date: Sun, 12 Jul 2026 11:24:04 -0400 Subject: [PATCH 1/3] Upgrade MCP SDK to 1.29 --- package-lock.json | 649 ++++++++++++++++++++++++++----------- package.json | 2 +- scripts/mcp-smoke-test.mjs | 64 ++++ 3 files changed, 518 insertions(+), 197 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7feadc2..4fa5706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@linear/sdk": "^86.0.0", - "@modelcontextprotocol/sdk": "^1.6.0", + "@modelcontextprotocol/sdk": "^1.29.0", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "cors": "^2.8.5", @@ -675,6 +675,18 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1183,28 +1195,50 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.7.0.tgz", - "integrity": "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", + "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^4.1.0", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" @@ -1213,125 +1247,129 @@ "node": ">= 0.6" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", - "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.5.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "dependencies": { - "side-channel": "^1.1.0" - }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", "engines": { - "node": ">=0.6" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dependencies": { - "safe-buffer": "5.2.1" - }, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", "engines": { "node": ">=6.6.0" } }, "node_modules/@modelcontextprotocol/sdk/node_modules/express": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", - "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.0.1", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", - "content-type": "~1.0.4", - "cookie": "0.7.1", + "content-type": "^1.0.5", + "cookie": "^0.7.1", "cookie-signature": "^1.2.1", - "debug": "4.3.6", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "^2.0.0", - "fresh": "2.0.0", - "http-errors": "2.0.0", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", - "methods": "~1.1.2", "mime-types": "^3.0.0", - "on-finished": "2.4.1", - "once": "1.4.0", - "parseurl": "~1.3.3", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "router": "^2.0.0", - "safe-buffer": "5.2.1", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", "send": "^1.1.0", - "serve-static": "^2.1.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "^2.0.0", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { "node": ">= 18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dependencies": { - "ms": "2.1.2" }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/express/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -1341,32 +1379,69 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", - "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -1375,6 +1450,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -1386,103 +1462,153 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", "dependencies": { - "mime-db": "^1.53.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/@modelcontextprotocol/sdk/node_modules/send": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", - "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", "dependencies": { - "debug": "^4.3.5", - "destroy": "^1.2.0", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", - "fresh": "^0.5.2", - "http-errors": "^2.0.0", - "mime-types": "^2.1.35", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/send/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", - "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", - "send": "^1.0.0" + "send": "^1.2.0" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", - "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" } }, "node_modules/@sinclair/typebox": { @@ -1804,6 +1930,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -2423,7 +2588,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2434,9 +2598,10 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -2951,9 +3116,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, "engines": { "node": ">= 16" }, @@ -2961,7 +3130,7 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" + "express": ">= 4.11" } }, "node_modules/express/node_modules/debug": { @@ -2980,8 +3149,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", @@ -2995,6 +3163,22 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -3360,6 +3544,15 @@ "node": ">=18.0.0" } }, + "node_modules/hono": { + "version": "4.12.29", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.29.tgz", + "integrity": "sha512-1hNiRjawYrLq/4m3DQQjPGFg0VZkk4RjQJDff/excI6Dm9BiL75qxGrd7/c6YOxPdq6AscP3LiXhQ6fKFC1Waw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3476,6 +3669,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3567,7 +3769,8 @@ "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" }, "node_modules/is-stream": { "version": "2.0.1", @@ -3584,8 +3787,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -4250,6 +4452,15 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4298,6 +4509,12 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -4824,7 +5041,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -4868,9 +5084,10 @@ } }, "node_modules/pkce-challenge": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", - "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", "engines": { "node": ">=16.20.0" } @@ -5068,28 +5285,63 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, "node_modules/react-is": { @@ -5118,6 +5370,15 @@ "node": ">=0.10.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", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -5178,10 +5439,13 @@ } }, "node_modules/router": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", - "integrity": "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" @@ -5191,11 +5455,13 @@ } }, "node_modules/router/node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "engines": { - "node": ">=16" + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/safe-buffer": { @@ -5298,7 +5564,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5310,19 +5575,19 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -5334,12 +5599,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -5890,7 +6156,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -6011,14 +6276,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", - "peerDependencies": { - "zod": "^3.24.1" - } } } } diff --git a/package.json b/package.json index be95bbe..d188848 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "@linear/sdk": "^86.0.0", - "@modelcontextprotocol/sdk": "^1.6.0", + "@modelcontextprotocol/sdk": "^1.29.0", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "cors": "^2.8.5", diff --git a/scripts/mcp-smoke-test.mjs b/scripts/mcp-smoke-test.mjs index b2b4c92..30cbbc3 100644 --- a/scripts/mcp-smoke-test.mjs +++ b/scripts/mcp-smoke-test.mjs @@ -37,6 +37,19 @@ async function main() { try { await client.connect(transport); + const serverVersion = client.getServerVersion(); + assert.equal(serverVersion?.name, 'linear', 'Server must identify itself as "linear".'); + assert.match( + serverVersion?.version ?? '', + /^\d+\.\d+\.\d+/, + 'Server must report a semver version during initialization.', + ); + + const serverCapabilities = client.getServerCapabilities(); + assert.ok(serverCapabilities?.tools, 'Server must declare the tools capability.'); + assert.ok(serverCapabilities?.resources, 'Server must declare the resources capability.'); + assert.ok(serverCapabilities?.prompts, 'Server must declare the prompts capability.'); + const { tools } = await client.listTools(); const actualToolNames = tools.map((tool) => tool.name).sort(); const expectedToolNames = allToolDefinitions.map((tool) => tool.name).sort(); @@ -46,6 +59,11 @@ async function main() { expectedToolNames, `MCP server advertised an unexpected tool set. Expected ${expectedToolNames.length} tools, got ${actualToolNames.length}.`, ); + assert.equal( + new Set(actualToolNames).size, + actualToolNames.length, + 'MCP server advertised duplicate tool names.', + ); for (const tool of tools) { assert.equal( @@ -66,6 +84,52 @@ async function main() { assert.ok(actualToolNames.includes(toolName), `Expected tool ${toolName} to be registered.`); } + const statusResult = await client.callTool({ + name: 'linear_getServerStatus', + arguments: {}, + }); + assert.equal(statusResult.isError, false, 'Server status tool should run without Linear I/O.'); + assert.ok(Array.isArray(statusResult.content), 'Tool results must carry a content array.'); + assert.equal(statusResult.content[0].type, 'text', 'Tool results must be text content items.'); + const statusPayload = JSON.parse(statusResult.content[0].text); + assert.equal(statusPayload.toolCount, actualToolNames.length); + assert.match( + statusPayload.version ?? '', + /^\d+\.\d+\.\d+/, + 'Server status must report a semver version.', + ); + + const rejectedArguments = await client.callTool({ + name: 'linear_getServerStatus', + arguments: { unexpectedArgument: true }, + }); + assert.equal( + rejectedArguments.isError, + true, + 'Unknown arguments must surface an in-band error result rather than a protocol error.', + ); + assert.equal( + rejectedArguments.content[0].type, + 'text', + 'Error results must be text content items, not protocol-level errors.', + ); + assert.ok( + rejectedArguments.content[0].text.includes( + 'Unknown argument(s) for linear_getServerStatus: unexpectedArgument', + ), + ); + + const unknownToolResult = await client.callTool({ + name: 'linear_toolThatDoesNotExist', + arguments: {}, + }); + assert.equal( + unknownToolResult.isError, + true, + 'Unknown tools must surface an in-band error result rather than a protocol error.', + ); + assert.ok(unknownToolResult.content[0].text.includes('Unknown tool: linear_toolThatDoesNotExist')); + const { resources } = await client.listResources(); const resourceUris = resources.map((resource) => resource.uri); assert.ok(resourceUris.includes('linear://viewer'), 'Expected linear://viewer resource to be registered.'); From c6099b985c4a52eb95599d06942aba1fd48f03ec Mon Sep 17 00:00:00 2001 From: itz4blitz Date: Sun, 12 Jul 2026 10:43:22 -0400 Subject: [PATCH 2/3] Fix resource content type narrowing in mcp-resources test --- src/__tests__/mcp-resources.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/__tests__/mcp-resources.test.ts b/src/__tests__/mcp-resources.test.ts index 09a28ec..8860679 100644 --- a/src/__tests__/mcp-resources.test.ts +++ b/src/__tests__/mcp-resources.test.ts @@ -43,7 +43,10 @@ describe('Linear MCP resources', () => { lastOperation: null, }), }); - expect(viewer.contents[0]).toMatchObject({ uri: 'linear://viewer', mimeType: 'application/json' }); + expect(viewer.contents[0]).toMatchObject({ + uri: 'linear://viewer', + mimeType: 'application/json', + }); await readLinearResource('linear://project/project-1', { linearService, @@ -92,6 +95,10 @@ describe('Linear MCP resources', () => { lastOperation: 'request', }), }); - expect(rateLimit.contents[0].text).toContain('"isBlocked": true'); + const rateLimitContent = rateLimit.contents[0]; + if (!('text' in rateLimitContent)) { + throw new Error('Expected linear://rate-limit to return text contents.'); + } + expect(rateLimitContent.text).toContain('"isBlocked": true'); }); }); From 4ec572b7967159679b9640bef3f7c7e66d7a32ca Mon Sep 17 00:00:00 2001 From: itz4blitz Date: Sun, 12 Jul 2026 11:35:31 -0400 Subject: [PATCH 3/3] Add tool annotations and structured output --- scripts/mcp-smoke-test.mjs | 114 ++++++++++++- src/__tests__/tool-annotations.test.ts | 199 ++++++++++++++++++++++ src/mcp-server.ts | 9 +- src/tool-annotations.ts | 67 ++++++++ src/tool-schema.ts | 52 ++++++ src/tools/definitions/customer-tools.ts | 21 +++ src/tools/definitions/cycle-tools.ts | 18 +- src/tools/definitions/document-tools.ts | 38 ++++- src/tools/definitions/initiative-tools.ts | 22 +++ src/tools/definitions/issue-tools.ts | 41 ++++- src/tools/definitions/milestone-tools.ts | 17 +- src/tools/definitions/ops-tools.ts | 40 +++-- src/tools/definitions/project-tools.ts | 37 +++- src/tools/definitions/release-tools.ts | 33 ++++ src/tools/definitions/roadmap-tools.ts | 11 ++ src/tools/definitions/server-tools.ts | 3 + src/tools/definitions/team-tools.ts | 67 +++++++- src/tools/definitions/template-tools.ts | 59 ++++++- src/tools/definitions/user-tools.ts | 5 + src/tools/definitions/view-tools.ts | 20 ++- src/types.ts | 13 ++ 21 files changed, 832 insertions(+), 54 deletions(-) create mode 100644 src/__tests__/tool-annotations.test.ts create mode 100644 src/tool-annotations.ts diff --git a/scripts/mcp-smoke-test.mjs b/scripts/mcp-smoke-test.mjs index 30cbbc3..16e5c77 100644 --- a/scripts/mcp-smoke-test.mjs +++ b/scripts/mcp-smoke-test.mjs @@ -78,8 +78,81 @@ async function main() { `Tool ${tool.name} exposes disallowed top-level schema key ${key}.`, ); } + + assert.ok(tool.annotations, `Tool ${tool.name} must advertise annotations.`); + for (const hint of ['readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint']) { + assert.equal( + typeof tool.annotations[hint], + 'boolean', + `Tool ${tool.name} must advertise an explicit boolean ${hint}.`, + ); + } + + assert.ok(tool.outputSchema, `Tool ${tool.name} must advertise an outputSchema.`); + assert.equal( + tool.outputSchema.type, + 'object', + `Tool ${tool.name} must expose a top-level object output schema.`, + ); + } + + const toolsByName = new Map(tools.map((tool) => [tool.name, tool])); + const expectedAnnotations = [ + [ + 'linear_getIssues', + { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + ], + [ + 'linear_createIssue', + { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + ], + [ + 'linear_deleteWebhook', + { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, + ], + [ + 'linear_logoutAllSessions', + { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }, + ], + [ + 'linear_updateIssue', + { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }, + ], + ]; + for (const [toolName, annotations] of expectedAnnotations) { + assert.deepEqual( + toolsByName.get(toolName)?.annotations, + annotations, + `Tool ${toolName} advertised unexpected annotations.`, + ); } + const openWorldToolNames = tools + .filter((tool) => tool.annotations.openWorldHint) + .map((tool) => tool.name); + assert.deepEqual( + openWorldToolNames, + [], + 'Every tool talks only to the configured Linear workspace, so none may advertise openWorldHint.', + ); + + const arrayOutputTool = toolsByName.get('linear_getIssues'); + assert.equal( + arrayOutputTool.outputSchema.properties.items.type, + 'array', + 'Array-producing tools must advertise their array schema wrapped under an items envelope.', + ); + assert.deepEqual( + arrayOutputTool.outputSchema.required, + ['items'], + 'The items envelope must be required for array-producing tools.', + ); + for (const toolName of criticalToolNames) { assert.ok(actualToolNames.includes(toolName), `Expected tool ${toolName} to be registered.`); } @@ -98,6 +171,28 @@ async function main() { /^\d+\.\d+\.\d+/, 'Server status must report a semver version.', ); + // The SDK client has already validated structuredContent against the advertised + // outputSchema during callTool; here we pin that it mirrors the text payload. + assert.deepEqual( + statusResult.structuredContent, + statusPayload, + 'structuredContent must mirror the JSON text payload for object-producing tools.', + ); + + const rateLimitResult = await client.callTool({ + name: 'linear_getRateLimitStatus', + arguments: {}, + }); + assert.equal( + rateLimitResult.isError, + false, + 'Rate-limit status should run without Linear I/O.', + ); + assert.deepEqual( + rateLimitResult.structuredContent, + JSON.parse(rateLimitResult.content[0].text), + 'structuredContent must mirror the JSON text payload for linear_getRateLimitStatus.', + ); const rejectedArguments = await client.callTool({ name: 'linear_getServerStatus', @@ -118,6 +213,11 @@ async function main() { 'Unknown argument(s) for linear_getServerStatus: unexpectedArgument', ), ); + assert.equal( + rejectedArguments.structuredContent, + undefined, + 'Error results must stay text-only without structuredContent.', + ); const unknownToolResult = await client.callTool({ name: 'linear_toolThatDoesNotExist', @@ -128,12 +228,20 @@ async function main() { true, 'Unknown tools must surface an in-band error result rather than a protocol error.', ); - assert.ok(unknownToolResult.content[0].text.includes('Unknown tool: linear_toolThatDoesNotExist')); + assert.ok( + unknownToolResult.content[0].text.includes('Unknown tool: linear_toolThatDoesNotExist'), + ); const { resources } = await client.listResources(); const resourceUris = resources.map((resource) => resource.uri); - assert.ok(resourceUris.includes('linear://viewer'), 'Expected linear://viewer resource to be registered.'); - assert.ok(resourceUris.includes('linear://rate-limit'), 'Expected linear://rate-limit resource to be registered.'); + assert.ok( + resourceUris.includes('linear://viewer'), + 'Expected linear://viewer resource to be registered.', + ); + assert.ok( + resourceUris.includes('linear://rate-limit'), + 'Expected linear://rate-limit resource to be registered.', + ); const guideResource = await client.readResource({ uri: 'linear://resource-guide' }); assert.ok(guideResource.contents[0].text.includes('linear://project/{id}')); diff --git a/src/__tests__/tool-annotations.test.ts b/src/__tests__/tool-annotations.test.ts new file mode 100644 index 0000000..14b1318 --- /dev/null +++ b/src/__tests__/tool-annotations.test.ts @@ -0,0 +1,199 @@ +import { allToolDefinitions } from '../tools/definitions/index.js'; +import { buildStructuredContent, convertToolDefinition } from '../tool-schema.js'; +import { MCPToolDefinition } from '../types.js'; + +function getDefinition(name: string): MCPToolDefinition { + const definition = allToolDefinitions.find((toolDefinition) => toolDefinition.name === name); + if (!definition) { + throw new Error(`Expected tool definition ${name} to be registered.`); + } + return definition; +} + +describe('tool annotations', () => { + it('declares explicit annotations on every tool definition', () => { + expect(allToolDefinitions.length).toBeGreaterThanOrEqual(185); + + for (const toolDefinition of allToolDefinitions) { + const annotations = toolDefinition.annotations; + expect(annotations).toBeDefined(); + expect(typeof annotations.readOnlyHint).toBe('boolean'); + expect(typeof annotations.destructiveHint).toBe('boolean'); + expect(typeof annotations.idempotentHint).toBe('boolean'); + expect(typeof annotations.openWorldHint).toBe('boolean'); + } + }); + + it('keeps annotation combinations coherent', () => { + for (const toolDefinition of allToolDefinitions) { + const { readOnlyHint, destructiveHint, idempotentHint } = toolDefinition.annotations; + + // A read-only tool can never be destructive and is always idempotent. + if (readOnlyHint) { + expect(destructiveHint).toBe(false); + expect(idempotentHint).toBe(true); + } + } + }); + + it('classifies reads, searches, and local generators as read-only and idempotent', () => { + const readOnlyNames = allToolDefinitions + .map((toolDefinition) => toolDefinition.name) + .filter((name) => /^linear_(get|search)/.test(name)); + + expect(readOnlyNames).toContain('linear_getIssues'); + + for (const name of readOnlyNames) { + expect({ name, annotations: getDefinition(name).annotations }).toEqual({ + name, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }); + } + }); + + it('classifies deletes, archives, and logouts as destructive but idempotent', () => { + const destructiveNames = allToolDefinitions + .map((toolDefinition) => toolDefinition.name) + .filter((name) => /^linear_(delete|archive|logout)/.test(name)); + + expect(destructiveNames).toContain('linear_deleteWebhook'); + expect(destructiveNames).toContain('linear_archiveIssue'); + expect(destructiveNames).toContain('linear_logoutAllSessions'); + + for (const name of destructiveNames) { + expect({ name, annotations: getDefinition(name).annotations }).toEqual({ + name, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: false, + }, + }); + } + }); + + it('classifies creators as additive and non-idempotent', () => { + const creatorNames = allToolDefinitions + .map((toolDefinition) => toolDefinition.name) + .filter((name) => /^linear_create/.test(name)); + creatorNames.push('linear_duplicateIssue', 'linear_addAttachment'); + + expect(creatorNames).toContain('linear_createIssue'); + + for (const name of creatorNames) { + expect({ name, annotations: getDefinition(name).annotations }).toEqual({ + name, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }); + } + }); + + it('classifies absolute setters and reversible membership changes as idempotent mutations', () => { + for (const name of [ + 'linear_updateIssue', + 'linear_assignIssue', + 'linear_setIssuePriority', + 'linear_markNotificationAsRead', + 'linear_addToFavorites', + 'linear_addIssueLabel', + 'linear_removeIssueLabel', + 'linear_unarchiveDocument', + 'linear_subscribeToIssue', + ]) { + expect({ name, annotations: getDefinition(name).annotations }).toEqual({ + name, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }); + } + }); + + it('declares every tool as closed-world', () => { + const openWorldNames = allToolDefinitions + .filter((toolDefinition) => toolDefinition.annotations.openWorldHint) + .map((toolDefinition) => toolDefinition.name); + + expect(openWorldNames).toEqual([]); + }); +}); + +describe('convertToolDefinition wire format', () => { + it('emits annotations for every tool', () => { + for (const toolDefinition of allToolDefinitions) { + const converted = convertToolDefinition(toolDefinition); + expect(converted.annotations).toEqual(toolDefinition.annotations); + } + }); + + it('emits a top-level object outputSchema for every tool', () => { + for (const toolDefinition of allToolDefinitions) { + const converted = convertToolDefinition(toolDefinition); + expect(converted.outputSchema).toBeDefined(); + expect(converted.outputSchema?.type).toBe('object'); + } + }); + + it('passes object output schemas through unchanged', () => { + const toolDefinition = getDefinition('linear_getRateLimitStatus'); + const converted = convertToolDefinition(toolDefinition); + + expect(converted.outputSchema).toEqual({ + type: 'object', + properties: toolDefinition.output_schema.properties, + }); + }); + + it('wraps array output schemas in a required items envelope', () => { + const toolDefinition = getDefinition('linear_getIssues'); + expect(toolDefinition.output_schema.type).toBe('array'); + + const converted = convertToolDefinition(toolDefinition); + + expect(converted.outputSchema).toEqual({ + type: 'object', + properties: { + items: toolDefinition.output_schema, + }, + required: ['items'], + }); + }); +}); + +describe('buildStructuredContent', () => { + it('returns object results as-is for object output schemas', () => { + const toolDefinition = getDefinition('linear_getRateLimitStatus'); + const result = { blockedUntil: 0, isBlocked: false }; + + expect(buildStructuredContent(toolDefinition, result)).toEqual(result); + }); + + it('wraps array results in an items envelope for array output schemas', () => { + const toolDefinition = getDefinition('linear_getIssues'); + const result = [{ id: 'issue-1' }, { id: 'issue-2' }]; + + expect(buildStructuredContent(toolDefinition, result)).toEqual({ items: result }); + }); + + it('returns undefined when the result does not match the advertised shape', () => { + expect(buildStructuredContent(getDefinition('linear_getIssues'), { id: 'x' })).toBeUndefined(); + expect( + buildStructuredContent(getDefinition('linear_getRateLimitStatus'), null), + ).toBeUndefined(); + expect(buildStructuredContent(getDefinition('linear_getRateLimitStatus'), [1])).toBeUndefined(); + }); +}); diff --git a/src/mcp-server.ts b/src/mcp-server.ts index 2d40a35..e0b1609 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -13,7 +13,7 @@ import { type Resource, } from '@modelcontextprotocol/sdk/types.js'; import { MCPToolDefinition } from './types.js'; -import { convertToolDefinition } from './tool-schema.js'; +import { buildStructuredContent, convertToolDefinition } from './tool-schema.js'; import { validateToolArguments } from './tools/argument-validation.js'; import pkg from '../package.json' with { type: 'json' }; @@ -62,6 +62,7 @@ export async function runMCPServer(config: MCPServerConfig) { // Convert our tool definitions to the SDK format const sdkTools = config.tools.map(convertToolDefinition); + const toolDefinitionsByName = new Map(config.tools.map((toolDef) => [toolDef.name, toolDef])); // Handle list tools request server.setRequestHandler(ListToolsRequestSchema, async () => ({ @@ -98,8 +99,14 @@ export async function runMCPServer(config: MCPServerConfig) { // Call the handler const result = await config.handleRequest({ name, args }); + // Mirror the JSON text payload as structuredContent matching the + // advertised outputSchema (array results are wrapped as { items }). + const toolDef = toolDefinitionsByName.get(name); + const structuredContent = toolDef ? buildStructuredContent(toolDef, result) : undefined; + return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + ...(structuredContent !== undefined ? { structuredContent } : {}), isError: false, }; } catch (error) { diff --git a/src/tool-annotations.ts b/src/tool-annotations.ts new file mode 100644 index 0000000..300b5f7 --- /dev/null +++ b/src/tool-annotations.ts @@ -0,0 +1,67 @@ +import { ToolAnnotations } from './types.js'; + +type ToolAnnotationOverrides = Partial; + +/** + * Pure reads (get, list, and search tools) and local generators that never + * mutate the Linear workspace. Safe to repeat and safe for clients to + * auto-allow. + */ +export function readOnlyToolAnnotations(overrides: ToolAnnotationOverrides = {}): ToolAnnotations { + return { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + ...overrides, + }; +} + +/** + * Creators: every call adds a new entity (issue, comment, webhook, ...), so + * repeating the same call is not idempotent, but nothing is destroyed. + */ +export function additiveToolAnnotations(overrides: ToolAnnotationOverrides = {}): ToolAnnotations { + return { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + ...overrides, + }; +} + +/** + * Absolute setters and reversible membership changes (update*, assign, add/ + * remove label or member, mark-as-read, unarchive, ...): repeating the same + * call with the same arguments has no additional effect. + */ +export function idempotentMutationToolAnnotations( + overrides: ToolAnnotationOverrides = {}, +): ToolAnnotations { + return { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + ...overrides, + }; +} + +/** + * Deletes, archives, session logouts, and secret rotations: destroys or + * invalidates existing state. Defaults to idempotent (deleting the same + * entity twice has no further effect); rotations override idempotentHint to + * false because each call mints a new secret and invalidates the previous one. + */ +export function destructiveToolAnnotations( + overrides: ToolAnnotationOverrides = {}, +): ToolAnnotations { + return { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: false, + ...overrides, + }; +} diff --git a/src/tool-schema.ts b/src/tool-schema.ts index a488682..d2f7512 100644 --- a/src/tool-schema.ts +++ b/src/tool-schema.ts @@ -3,6 +3,35 @@ import { MCPToolDefinition } from './types.js'; const DISALLOWED_TOP_LEVEL_KEYS = ['oneOf', 'anyOf', 'allOf', 'enum', 'not'] as const; +/** + * Convert an internal output_schema to the MCP outputSchema wire format. + * + * MCP requires outputSchema (and structuredContent) to be a JSON object at the + * top level, but many tools return arrays. Array schemas are wrapped in a + * required `items` envelope; buildStructuredContent applies the matching + * wrapping to results so structuredContent always validates against the + * advertised schema. + */ +function convertOutputSchema( + outputSchema: MCPToolDefinition['output_schema'], +): Tool['outputSchema'] { + if (outputSchema.type === 'array') { + return { + type: 'object', + properties: { + items: outputSchema as Record, + }, + required: ['items'], + }; + } + + const { type: _type, ...schemaWithoutType } = outputSchema; + return { + type: 'object', + ...schemaWithoutType, + }; +} + /** * Convert MCPToolDefinition to the MCP SDK Tool format */ @@ -24,5 +53,28 @@ export function convertToolDefinition(toolDef: MCPToolDefinition): Tool { type: 'object', ...sanitizedSchema, }, + outputSchema: convertOutputSchema(toolDef.output_schema), + annotations: toolDef.annotations, }; } + +/** + * Build the structuredContent payload for a successful tool result so it + * matches the outputSchema advertised by convertToolDefinition: object results + * pass through as-is, array results are wrapped in the `items` envelope. + * Returns undefined when the result does not match the advertised shape. + */ +export function buildStructuredContent( + toolDef: MCPToolDefinition, + result: unknown, +): Record | undefined { + if (toolDef.output_schema.type === 'array') { + return Array.isArray(result) ? { items: result } : undefined; + } + + if (result !== null && typeof result === 'object' && !Array.isArray(result)) { + return result as Record; + } + + return undefined; +} diff --git a/src/tools/definitions/customer-tools.ts b/src/tools/definitions/customer-tools.ts index ee87d2b..990144a 100644 --- a/src/tools/definitions/customer-tools.ts +++ b/src/tools/definitions/customer-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const positiveLimitSchema = { type: 'integer', minimum: 1 }; const orderBySchema = { type: 'string', enum: ['createdAt', 'updatedAt'] }; @@ -123,6 +129,7 @@ const customerNeedCreateProperties = { export const customerToolDefinitions: MCPToolDefinition[] = [ { name: 'linear_getCustomers', + annotations: readOnlyToolAnnotations(), description: 'Get customers from Linear', input_schema: { type: 'object', @@ -141,6 +148,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_getCustomerById', + annotations: readOnlyToolAnnotations(), description: 'Get a customer by ID', input_schema: { type: 'object', @@ -151,6 +159,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_createCustomer', + annotations: additiveToolAnnotations(), description: 'Create a customer', input_schema: { type: 'object', @@ -161,6 +170,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_updateCustomer', + annotations: idempotentMutationToolAnnotations(), description: 'Update a customer', input_schema: { type: 'object', @@ -174,6 +184,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_deleteCustomer', + annotations: destructiveToolAnnotations(), description: 'Delete a customer', input_schema: { type: 'object', @@ -184,6 +195,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_getCustomerNeeds', + annotations: readOnlyToolAnnotations(), description: 'Get customer needs from Linear', input_schema: { type: 'object', @@ -201,6 +213,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_getCustomerNeedById', + annotations: readOnlyToolAnnotations(), description: 'Get a customer need by ID', input_schema: { type: 'object', @@ -211,6 +224,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_createCustomerNeed', + annotations: additiveToolAnnotations(), description: 'Create a customer need linked to an issue or project', input_schema: { type: 'object', @@ -220,6 +234,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_createCustomerNeedFromAttachment', + annotations: additiveToolAnnotations(), description: 'Create or unarchive a customer need from an existing attachment', input_schema: { type: 'object', @@ -235,6 +250,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_updateCustomerNeed', + annotations: idempotentMutationToolAnnotations(), description: 'Update a customer need', input_schema: { type: 'object', @@ -258,6 +274,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_archiveCustomerNeed', + annotations: destructiveToolAnnotations(), description: 'Archive a customer need', input_schema: { type: 'object', @@ -268,6 +285,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_unarchiveCustomerNeed', + annotations: idempotentMutationToolAnnotations(), description: 'Restore an archived customer need', input_schema: { type: 'object', @@ -278,6 +296,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_deleteCustomerNeed', + annotations: destructiveToolAnnotations(), description: 'Delete a customer need', input_schema: { type: 'object', @@ -288,6 +307,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_getCustomerStatuses', + annotations: readOnlyToolAnnotations(), description: 'Get customer statuses from Linear', input_schema: { type: 'object', @@ -301,6 +321,7 @@ export const customerToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_getCustomerTiers', + annotations: readOnlyToolAnnotations(), description: 'Get customer tiers from Linear', input_schema: { type: 'object', diff --git a/src/tools/definitions/cycle-tools.ts b/src/tools/definitions/cycle-tools.ts index 6938e2f..6efccc2 100644 --- a/src/tools/definitions/cycle-tools.ts +++ b/src/tools/definitions/cycle-tools.ts @@ -1,4 +1,9 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const positiveLimitSchema = { type: 'integer', @@ -102,6 +107,7 @@ const cycleOutputSchema = { */ export const getCyclesToolDefinition: MCPToolDefinition = { name: 'linear_getCycles', + annotations: readOnlyToolAnnotations(), description: 'Get a list of all cycles', input_schema: { type: 'object', @@ -129,6 +135,7 @@ export const getCyclesToolDefinition: MCPToolDefinition = { */ export const getActiveCycleToolDefinition: MCPToolDefinition = { name: 'linear_getActiveCycle', + annotations: readOnlyToolAnnotations(), description: 'Get the currently active cycle for a team', input_schema: { type: 'object', @@ -153,6 +160,7 @@ export const getActiveCycleToolDefinition: MCPToolDefinition = { export const getCycleByIdToolDefinition: MCPToolDefinition = { name: 'linear_getCycleById', + annotations: readOnlyToolAnnotations(), description: 'Get details of a specific cycle', input_schema: { type: 'object', @@ -166,6 +174,7 @@ export const getCycleByIdToolDefinition: MCPToolDefinition = { export const createCycleToolDefinition: MCPToolDefinition = { name: 'linear_createCycle', + annotations: additiveToolAnnotations(), description: 'Create a new cycle', input_schema: { type: 'object', @@ -184,6 +193,7 @@ export const createCycleToolDefinition: MCPToolDefinition = { export const updateCycleToolDefinition: MCPToolDefinition = { name: 'linear_updateCycle', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing cycle. Provide id plus at least one other field to change.', input_schema: { type: 'object', @@ -202,6 +212,7 @@ export const updateCycleToolDefinition: MCPToolDefinition = { export const completeCycleToolDefinition: MCPToolDefinition = { name: 'linear_completeCycle', + annotations: idempotentMutationToolAnnotations(), description: 'Mark a cycle as complete', input_schema: { type: 'object', @@ -215,6 +226,7 @@ export const completeCycleToolDefinition: MCPToolDefinition = { export const getCycleStatsToolDefinition: MCPToolDefinition = { name: 'linear_getCycleStats', + annotations: readOnlyToolAnnotations(), description: 'Get statistics for a cycle', input_schema: { type: 'object', @@ -242,7 +254,9 @@ export const getCycleStatsToolDefinition: MCPToolDefinition = { export const getCycleIssuesToolDefinition: MCPToolDefinition = { name: 'linear_getCycleIssues', - description: 'Get issues in a cycle, with PM-friendly filters like state, assignee, labels, and completion status', + annotations: readOnlyToolAnnotations(), + description: + 'Get issues in a cycle, with PM-friendly filters like state, assignee, labels, and completion status', input_schema: { type: 'object', properties: { @@ -292,6 +306,7 @@ export const getCycleIssuesToolDefinition: MCPToolDefinition = { */ export const addIssueToCycleToolDefinition: MCPToolDefinition = { name: 'linear_addIssueToCycle', + annotations: idempotentMutationToolAnnotations(), description: 'Add an issue to a cycle', input_schema: { type: 'object', @@ -336,6 +351,7 @@ export const addIssueToCycleToolDefinition: MCPToolDefinition = { */ export const removeIssueFromCycleToolDefinition: MCPToolDefinition = { name: 'linear_removeIssueFromCycle', + annotations: idempotentMutationToolAnnotations(), description: 'Remove an issue from a cycle', input_schema: { type: 'object', diff --git a/src/tools/definitions/document-tools.ts b/src/tools/definitions/document-tools.ts index a8aa5c1..8bc1343 100644 --- a/src/tools/definitions/document-tools.ts +++ b/src/tools/definitions/document-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const positiveLimitSchema = { type: 'integer', @@ -90,6 +96,7 @@ const documentOutputSchema = { export const getDocumentsToolDefinition: MCPToolDefinition = { name: 'linear_getDocuments', + annotations: readOnlyToolAnnotations(), description: 'Get workspace documents from Linear', input_schema: { type: 'object', @@ -144,6 +151,7 @@ export const getDocumentsToolDefinition: MCPToolDefinition = { export const getDocumentByIdToolDefinition: MCPToolDefinition = { name: 'linear_getDocumentById', + annotations: readOnlyToolAnnotations(), description: 'Get details of a specific Linear document', input_schema: { type: 'object', @@ -160,6 +168,7 @@ export const getDocumentByIdToolDefinition: MCPToolDefinition = { export const getProjectDocumentsToolDefinition: MCPToolDefinition = { name: 'linear_getProjectDocuments', + annotations: readOnlyToolAnnotations(), description: 'Get documents for a specific Linear project', input_schema: { type: 'object', @@ -195,6 +204,7 @@ export const getProjectDocumentsToolDefinition: MCPToolDefinition = { export const getInitiativeDocumentsToolDefinition: MCPToolDefinition = { name: 'linear_getInitiativeDocuments', + annotations: readOnlyToolAnnotations(), description: 'Get documents for a specific Linear initiative', input_schema: { type: 'object', @@ -230,6 +240,7 @@ export const getInitiativeDocumentsToolDefinition: MCPToolDefinition = { export const getTeamDocumentsToolDefinition: MCPToolDefinition = { name: 'linear_getTeamDocuments', + annotations: readOnlyToolAnnotations(), description: 'Get documents associated with a specific Linear team', input_schema: { type: 'object', @@ -271,6 +282,8 @@ function createParentDocumentToolDefinition( ): MCPToolDefinition { return { name, + // Every parent-scoped document tool is a pure read. + annotations: readOnlyToolAnnotations(), description, input_schema: { type: 'object', @@ -368,6 +381,7 @@ const pinnedResourceSchema = { export const getTeamResourcesToolDefinition: MCPToolDefinition = { name: 'linear_getTeamResources', + annotations: readOnlyToolAnnotations(), description: 'Get team home resource sections and pinned documents or external links', input_schema: { type: 'object', @@ -417,6 +431,7 @@ export const getTeamResourcesToolDefinition: MCPToolDefinition = { export const searchDocumentsToolDefinition: MCPToolDefinition = { name: 'linear_searchDocuments', + annotations: readOnlyToolAnnotations(), description: 'Search Linear documents by term', input_schema: { type: 'object', @@ -472,6 +487,7 @@ export const searchDocumentsToolDefinition: MCPToolDefinition = { export const getDocumentContentHistoryToolDefinition: MCPToolDefinition = { name: 'linear_getDocumentContentHistory', + annotations: readOnlyToolAnnotations(), description: 'Get content history entries for a Linear document content record', input_schema: { type: 'object', @@ -505,6 +521,7 @@ export const getDocumentContentHistoryToolDefinition: MCPToolDefinition = { export const createDocumentToolDefinition: MCPToolDefinition = { name: 'linear_createDocument', + annotations: additiveToolAnnotations(), description: 'Create a new Linear document', input_schema: { type: 'object', @@ -514,13 +531,22 @@ export const createDocumentToolDefinition: MCPToolDefinition = { icon: { type: 'string', description: 'Icon for the document' }, color: { type: 'string', description: 'Icon color for the document' }, projectId: { type: 'string', description: 'Optional project associated with the document' }, - initiativeId: { type: 'string', description: 'Optional initiative associated with the document' }, + initiativeId: { + type: 'string', + description: 'Optional initiative associated with the document', + }, teamId: { type: 'string', description: 'Optional team associated with the document' }, issueId: { type: 'string', description: 'Optional issue associated with the document' }, releaseId: { type: 'string', description: 'Optional release associated with the document' }, cycleId: { type: 'string', description: 'Optional cycle associated with the document' }, - resourceFolderId: { type: 'string', description: 'Optional resource folder containing the document' }, - lastAppliedTemplateId: { type: 'string', description: 'Optional template last applied to the document' }, + resourceFolderId: { + type: 'string', + description: 'Optional resource folder containing the document', + }, + lastAppliedTemplateId: { + type: 'string', + description: 'Optional template last applied to the document', + }, sortOrder: { type: 'number', description: 'Optional sort order in the resources list' }, }, required: ['title'], @@ -530,7 +556,9 @@ export const createDocumentToolDefinition: MCPToolDefinition = { export const updateDocumentToolDefinition: MCPToolDefinition = { name: 'linear_updateDocument', - description: 'Update an existing Linear document. Provide id plus at least one other field to change.', + annotations: idempotentMutationToolAnnotations(), + description: + 'Update an existing Linear document. Provide id plus at least one other field to change.', input_schema: { type: 'object', properties: { @@ -594,6 +622,7 @@ export const updateDocumentToolDefinition: MCPToolDefinition = { export const archiveDocumentToolDefinition: MCPToolDefinition = { name: 'linear_archiveDocument', + annotations: destructiveToolAnnotations(), description: 'Archive (trash) a Linear document', input_schema: { type: 'object', @@ -613,6 +642,7 @@ export const archiveDocumentToolDefinition: MCPToolDefinition = { export const unarchiveDocumentToolDefinition: MCPToolDefinition = { name: 'linear_unarchiveDocument', + annotations: idempotentMutationToolAnnotations(), description: 'Restore a previously archived Linear document', input_schema: { type: 'object', diff --git a/src/tools/definitions/initiative-tools.ts b/src/tools/definitions/initiative-tools.ts index ec04968..091b576 100644 --- a/src/tools/definitions/initiative-tools.ts +++ b/src/tools/definitions/initiative-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const initiativeUpdateOutputSchema = { type: 'object', @@ -33,6 +39,7 @@ const initiativeUpdateArchiveOutputSchema = { export const initiativeToolDefinitions: MCPToolDefinition[] = [ { name: 'linear_getInitiatives', + annotations: readOnlyToolAnnotations(), description: 'Get a list of all initiatives from Linear', input_schema: { type: 'object', @@ -78,6 +85,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_getInitiativeById', + annotations: readOnlyToolAnnotations(), description: 'Get details of a specific initiative by ID', input_schema: { type: 'object', @@ -131,6 +139,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_createInitiative', + annotations: additiveToolAnnotations(), description: 'Create a new initiative', input_schema: { type: 'object', @@ -184,6 +193,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_updateInitiative', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing initiative', input_schema: { type: 'object', @@ -241,6 +251,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_archiveInitiative', + annotations: destructiveToolAnnotations(), description: 'Archive an initiative', input_schema: { type: 'object', @@ -261,6 +272,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_unarchiveInitiative', + annotations: idempotentMutationToolAnnotations(), description: 'Unarchive an initiative', input_schema: { type: 'object', @@ -281,6 +293,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_deleteInitiative', + annotations: destructiveToolAnnotations(), description: 'Delete an initiative (move to trash)', input_schema: { type: 'object', @@ -301,6 +314,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_getInitiativeProjects', + annotations: readOnlyToolAnnotations(), description: 'Get all projects associated with an initiative', input_schema: { type: 'object', @@ -346,6 +360,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_getInitiativeUpdateById', + annotations: readOnlyToolAnnotations(), description: 'Get a specific initiative update by ID', input_schema: { type: 'object', @@ -358,6 +373,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_getInitiativeUpdates', + annotations: readOnlyToolAnnotations(), description: 'Get updates for an initiative', input_schema: { type: 'object', @@ -378,6 +394,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_createInitiativeUpdate', + annotations: additiveToolAnnotations(), description: 'Create a new initiative update', input_schema: { type: 'object', @@ -400,6 +417,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_updateInitiativeUpdate', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing initiative update', input_schema: { type: 'object', @@ -422,6 +440,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_archiveInitiativeUpdate', + annotations: destructiveToolAnnotations(), description: 'Archive an initiative update', input_schema: { type: 'object', @@ -434,6 +453,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_unarchiveInitiativeUpdate', + annotations: idempotentMutationToolAnnotations(), description: 'Restore an archived initiative update', input_schema: { type: 'object', @@ -446,6 +466,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_addProjectToInitiative', + annotations: idempotentMutationToolAnnotations(), description: 'Add a project to an initiative', input_schema: { type: 'object', @@ -484,6 +505,7 @@ export const initiativeToolDefinitions: MCPToolDefinition[] = [ }, { name: 'linear_removeProjectFromInitiative', + annotations: idempotentMutationToolAnnotations(), description: 'Remove a project from an initiative', input_schema: { type: 'object', diff --git a/src/tools/definitions/issue-tools.ts b/src/tools/definitions/issue-tools.ts index 20160a5..1c6e44d 100644 --- a/src/tools/definitions/issue-tools.ts +++ b/src/tools/definitions/issue-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const jsonValueSchema = { anyOf: [ @@ -55,6 +61,7 @@ const issueCustomFieldValueSchema = { */ export const getIssuesToolDefinition: MCPToolDefinition = { name: 'linear_getIssues', + annotations: readOnlyToolAnnotations(), description: 'Get a list of recent issues from Linear', input_schema: { type: 'object', @@ -117,6 +124,7 @@ export const getIssuesToolDefinition: MCPToolDefinition = { */ export const getIssueByIdToolDefinition: MCPToolDefinition = { name: 'linear_getIssueById', + annotations: readOnlyToolAnnotations(), description: 'Get a specific issue by ID or identifier (e.g., ABC-123)', input_schema: { type: 'object', @@ -178,6 +186,7 @@ export const getIssueByIdToolDefinition: MCPToolDefinition = { */ export const getCustomFieldsToolDefinition: MCPToolDefinition = { name: 'linear_getCustomFields', + annotations: readOnlyToolAnnotations(), description: 'Get the custom field definitions available in the authenticated Linear workspace', input_schema: { type: 'object', @@ -195,6 +204,7 @@ export const getCustomFieldsToolDefinition: MCPToolDefinition = { */ export const getIssueCustomFieldsToolDefinition: MCPToolDefinition = { name: 'linear_getIssueCustomFields', + annotations: readOnlyToolAnnotations(), description: 'Get the custom field values that are currently set on a specific issue', input_schema: { type: 'object', @@ -224,6 +234,7 @@ export const getIssueCustomFieldsToolDefinition: MCPToolDefinition = { */ export const updateIssueCustomFieldToolDefinition: MCPToolDefinition = { name: 'linear_updateIssueCustomField', + annotations: idempotentMutationToolAnnotations(), description: 'Set or clear a custom field value on an issue. Pass null as value to clear it.', input_schema: { type: 'object', @@ -262,6 +273,7 @@ export const updateIssueCustomFieldToolDefinition: MCPToolDefinition = { */ export const searchIssuesToolDefinition: MCPToolDefinition = { name: 'linear_searchIssues', + annotations: readOnlyToolAnnotations(), description: 'Search for issues with various filters', input_schema: { type: 'object', @@ -346,6 +358,7 @@ export const searchIssuesToolDefinition: MCPToolDefinition = { */ export const createIssueToolDefinition: MCPToolDefinition = { name: 'linear_createIssue', + annotations: additiveToolAnnotations(), description: 'Create a new issue in Linear', input_schema: { type: 'object', @@ -436,6 +449,7 @@ export const createIssueToolDefinition: MCPToolDefinition = { */ export const updateIssueToolDefinition: MCPToolDefinition = { name: 'linear_updateIssue', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing issue in Linear', input_schema: { type: 'object', @@ -536,6 +550,7 @@ export const updateIssueToolDefinition: MCPToolDefinition = { */ export const createCommentToolDefinition: MCPToolDefinition = { name: 'linear_createComment', + annotations: additiveToolAnnotations(), description: 'Add a comment to an issue, project, initiative, update, document content, or as a threaded reply', input_schema: { @@ -610,6 +625,7 @@ export const createCommentToolDefinition: MCPToolDefinition = { export const updateCommentToolDefinition: MCPToolDefinition = { name: 'linear_updateComment', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing comment', input_schema: { type: 'object', @@ -619,8 +635,15 @@ export const updateCommentToolDefinition: MCPToolDefinition = { quotedText: { type: 'string', description: 'Optional quoted text for inline comments' }, resolvingCommentId: { type: 'string', description: 'Optional resolving comment ID' }, resolvingUserId: { type: 'string', description: 'Optional resolving user ID' }, - subscriberIds: { type: 'array', items: { type: 'string' }, description: 'Optional subscriber IDs' }, - doNotSubscribeToIssue: { type: 'boolean', description: 'Prevent auto subscription on update' }, + subscriberIds: { + type: 'array', + items: { type: 'string' }, + description: 'Optional subscriber IDs', + }, + doNotSubscribeToIssue: { + type: 'boolean', + description: 'Prevent auto subscription on update', + }, }, required: ['id'], }, @@ -643,6 +666,7 @@ export const updateCommentToolDefinition: MCPToolDefinition = { export const deleteCommentToolDefinition: MCPToolDefinition = { name: 'linear_deleteComment', + annotations: destructiveToolAnnotations(), description: 'Delete a comment', input_schema: { type: 'object', @@ -665,6 +689,7 @@ export const deleteCommentToolDefinition: MCPToolDefinition = { */ export const addIssueLabelToolDefinition: MCPToolDefinition = { name: 'linear_addIssueLabel', + annotations: idempotentMutationToolAnnotations(), description: 'Add a label to an issue in Linear', input_schema: { type: 'object', @@ -695,6 +720,7 @@ export const addIssueLabelToolDefinition: MCPToolDefinition = { */ export const removeIssueLabelToolDefinition: MCPToolDefinition = { name: 'linear_removeIssueLabel', + annotations: idempotentMutationToolAnnotations(), description: 'Remove a label from an issue in Linear', input_schema: { type: 'object', @@ -725,6 +751,7 @@ export const removeIssueLabelToolDefinition: MCPToolDefinition = { */ export const assignIssueToolDefinition: MCPToolDefinition = { name: 'linear_assignIssue', + annotations: idempotentMutationToolAnnotations(), description: 'Assign an issue to a user', input_schema: { type: 'object', @@ -763,6 +790,7 @@ export const assignIssueToolDefinition: MCPToolDefinition = { */ export const subscribeToIssueToolDefinition: MCPToolDefinition = { name: 'linear_subscribeToIssue', + annotations: idempotentMutationToolAnnotations(), description: 'Subscribe to issue updates', input_schema: { type: 'object', @@ -788,6 +816,7 @@ export const subscribeToIssueToolDefinition: MCPToolDefinition = { */ export const convertIssueToSubtaskToolDefinition: MCPToolDefinition = { name: 'linear_convertIssueToSubtask', + annotations: idempotentMutationToolAnnotations(), description: 'Convert an issue to a subtask', input_schema: { type: 'object', @@ -826,6 +855,7 @@ export const convertIssueToSubtaskToolDefinition: MCPToolDefinition = { */ export const createIssueRelationToolDefinition: MCPToolDefinition = { name: 'linear_createIssueRelation', + annotations: additiveToolAnnotations(), description: 'Create relations between issues (blocks, is blocked by, etc.)', input_schema: { type: 'object', @@ -866,6 +896,7 @@ export const createIssueRelationToolDefinition: MCPToolDefinition = { export const deleteIssueRelationToolDefinition: MCPToolDefinition = { name: 'linear_deleteIssueRelation', + annotations: destructiveToolAnnotations(), description: 'Delete an issue relation by ID', input_schema: { type: 'object', @@ -891,6 +922,7 @@ export const deleteIssueRelationToolDefinition: MCPToolDefinition = { */ export const archiveIssueToolDefinition: MCPToolDefinition = { name: 'linear_archiveIssue', + annotations: destructiveToolAnnotations(), description: 'Archive an issue', input_schema: { type: 'object', @@ -916,6 +948,7 @@ export const archiveIssueToolDefinition: MCPToolDefinition = { */ export const setIssuePriorityToolDefinition: MCPToolDefinition = { name: 'linear_setIssuePriority', + annotations: idempotentMutationToolAnnotations(), description: 'Set the priority of an issue', input_schema: { type: 'object', @@ -955,6 +988,7 @@ export const setIssuePriorityToolDefinition: MCPToolDefinition = { */ export const transferIssueToolDefinition: MCPToolDefinition = { name: 'linear_transferIssue', + annotations: idempotentMutationToolAnnotations(), description: 'Transfer an issue to another team', input_schema: { type: 'object', @@ -993,6 +1027,7 @@ export const transferIssueToolDefinition: MCPToolDefinition = { */ export const duplicateIssueToolDefinition: MCPToolDefinition = { name: 'linear_duplicateIssue', + annotations: additiveToolAnnotations(), description: 'Duplicate an issue', input_schema: { type: 'object', @@ -1034,6 +1069,7 @@ export const duplicateIssueToolDefinition: MCPToolDefinition = { */ export const getIssueHistoryToolDefinition: MCPToolDefinition = { name: 'linear_getIssueHistory', + annotations: readOnlyToolAnnotations(), description: 'Get the history of changes made to an issue', input_schema: { type: 'object', @@ -1077,6 +1113,7 @@ export const getIssueHistoryToolDefinition: MCPToolDefinition = { */ export const getCommentsToolDefinition: MCPToolDefinition = { name: 'linear_getComments', + annotations: readOnlyToolAnnotations(), description: 'Get comments for an issue, project, initiative, update, or document content', input_schema: { type: 'object', diff --git a/src/tools/definitions/milestone-tools.ts b/src/tools/definitions/milestone-tools.ts index 088ce6a..4e32609 100644 --- a/src/tools/definitions/milestone-tools.ts +++ b/src/tools/definitions/milestone-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const milestoneOrderBySchema = { type: 'string', @@ -12,7 +18,9 @@ const milestoneStatusSchema = { export const getMilestonesToolDefinition: MCPToolDefinition = { name: 'linear_getMilestones', - description: 'Get project milestones from Linear, with PM-friendly filters for project, team, and milestone status', + annotations: readOnlyToolAnnotations(), + description: + 'Get project milestones from Linear, with PM-friendly filters for project, team, and milestone status', input_schema: { type: 'object', properties: { @@ -74,6 +82,7 @@ export const getMilestonesToolDefinition: MCPToolDefinition = { export const getMilestoneByIdToolDefinition: MCPToolDefinition = { name: 'linear_getMilestoneById', + annotations: readOnlyToolAnnotations(), description: 'Get details of a specific project milestone by ID', input_schema: { type: 'object', @@ -111,6 +120,7 @@ export const getMilestoneByIdToolDefinition: MCPToolDefinition = { export const createMilestoneToolDefinition: MCPToolDefinition = { name: 'linear_createMilestone', + annotations: additiveToolAnnotations(), description: 'Create a new project milestone in Linear', input_schema: { type: 'object', @@ -164,7 +174,9 @@ export const createMilestoneToolDefinition: MCPToolDefinition = { export const updateMilestoneToolDefinition: MCPToolDefinition = { name: 'linear_updateMilestone', - description: 'Update an existing project milestone in Linear. Provide id plus at least one other field to change.', + annotations: idempotentMutationToolAnnotations(), + description: + 'Update an existing project milestone in Linear. Provide id plus at least one other field to change.', input_schema: { type: 'object', properties: { @@ -221,6 +233,7 @@ export const updateMilestoneToolDefinition: MCPToolDefinition = { export const archiveMilestoneToolDefinition: MCPToolDefinition = { name: 'linear_archiveMilestone', + annotations: destructiveToolAnnotations(), description: 'Archive a project milestone', input_schema: { type: 'object', diff --git a/src/tools/definitions/ops-tools.ts b/src/tools/definitions/ops-tools.ts index 451c529..b8aace2 100644 --- a/src/tools/definitions/ops-tools.ts +++ b/src/tools/definitions/ops-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const positiveLimitSchema = { type: 'integer', minimum: 1 }; const paginationOrderBySchema = { type: 'string', enum: ['createdAt', 'updatedAt'] }; @@ -119,20 +125,20 @@ const integrationOutputSchema = { }, }; -export const getWebhooksToolDefinition: MCPToolDefinition = { name: 'linear_getWebhooks', description: 'Get a list of webhooks', input_schema: { type: 'object', properties: { teamId: { type: 'string' }, limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: webhookOutputSchema } }; -export const createWebhookToolDefinition: MCPToolDefinition = { name: 'linear_createWebhook', description: 'Create a webhook for integration events', input_schema: { type: 'object', properties: { url: { type: 'string' }, resourceTypes: { type: 'array', items: { type: 'string' } }, teamId: { type: 'string' }, enabled: { type: 'boolean' }, label: { type: 'string' }, secret: { type: 'string' }, allPublicTeams: { type: 'boolean' } }, required: ['url', 'resourceTypes'] }, output_schema: webhookOutputSchema }; -export const deleteWebhookToolDefinition: MCPToolDefinition = { name: 'linear_deleteWebhook', description: 'Delete a webhook', input_schema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, output_schema: { type: 'object', properties: { success: { type: 'boolean' }, id: { type: 'string' } } } }; -export const getAttachmentsToolDefinition: MCPToolDefinition = { name: 'linear_getAttachments', description: 'Get attachments for an issue', input_schema: { type: 'object', properties: { issueId: { type: 'string' }, limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } }, required: ['issueId'] }, output_schema: { type: 'array', items: attachmentOutputSchema } }; -export const addAttachmentToolDefinition: MCPToolDefinition = { name: 'linear_addAttachment', description: 'Add an attachment to an issue', input_schema: { type: 'object', properties: { issueId: { type: 'string' }, title: { type: 'string' }, url: { type: 'string' }, subtitle: { type: 'string' }, iconUrl: { type: 'string' }, metadata: { type: 'object', additionalProperties: true }, commentBody: { type: 'string' }, groupBySource: { type: 'boolean' } }, required: ['issueId', 'title', 'url'] }, output_schema: attachmentOutputSchema }; -export const getNotificationsToolDefinition: MCPToolDefinition = { name: 'linear_getNotifications', description: 'Get notifications for the current user', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: notificationOutputSchema } }; -export const markNotificationAsReadToolDefinition: MCPToolDefinition = { name: 'linear_markNotificationAsRead', description: 'Mark a notification as read', input_schema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, output_schema: { type: 'object', properties: { success: { type: 'boolean' }, id: { type: 'string' } } } }; -export const getSubscriptionsToolDefinition: MCPToolDefinition = { name: 'linear_getSubscriptions', description: 'Get subscriptions for the current user', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: subscriptionOutputSchema } }; -export const markAllNotificationsAsReadToolDefinition: MCPToolDefinition = { name: 'linear_markAllNotificationsAsRead', description: 'Mark all notifications as read', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema } } }, output_schema: { type: 'object', properties: { success: { type: 'boolean' }, count: { type: 'number' } } } }; -export const getUnreadNotificationCountToolDefinition: MCPToolDefinition = { name: 'linear_getUnreadNotificationCount', description: 'Get count of unread notifications', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema } } }, output_schema: { type: 'object', properties: { count: { type: 'number' } } } }; -export const getAuthenticationSessionsToolDefinition: MCPToolDefinition = { name: 'linear_getAuthenticationSessions', description: 'Get active authentication sessions', input_schema: { type: 'object', properties: {} }, output_schema: { type: 'array', items: authSessionOutputSchema } }; -export const logoutSessionToolDefinition: MCPToolDefinition = { name: 'linear_logoutSession', description: 'Revoke a specific session', input_schema: { type: 'object', properties: { sessionId: { type: 'string' } }, required: ['sessionId'] }, output_schema: { type: 'object', properties: { success: { type: 'boolean' } } } }; -export const logoutOtherSessionsToolDefinition: MCPToolDefinition = { name: 'linear_logoutOtherSessions', description: 'Revoke all other sessions', input_schema: { type: 'object', properties: {} }, output_schema: { type: 'object', properties: { success: { type: 'boolean' } } } }; -export const logoutAllSessionsToolDefinition: MCPToolDefinition = { name: 'linear_logoutAllSessions', description: 'Revoke all sessions', input_schema: { type: 'object', properties: {} }, output_schema: { type: 'object', properties: { success: { type: 'boolean' } } } }; -export const getOrganizationAuditEventsToolDefinition: MCPToolDefinition = { name: 'linear_getOrganizationAuditEvents', description: 'Get audit events for the organization', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: auditEntryOutputSchema } }; -export const getUserAuditEventsToolDefinition: MCPToolDefinition = { name: 'linear_getUserAuditEvents', description: 'Get audit events for a specific user', input_schema: { type: 'object', properties: { userId: { type: 'string' }, limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } }, required: ['userId'] }, output_schema: { type: 'array', items: auditEntryOutputSchema } }; -export const getIntegrationsToolDefinition: MCPToolDefinition = { name: 'linear_getIntegrations', description: 'Get a list of active integrations', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: integrationOutputSchema } }; +export const getWebhooksToolDefinition: MCPToolDefinition = { name: 'linear_getWebhooks', annotations: readOnlyToolAnnotations(), description: 'Get a list of webhooks', input_schema: { type: 'object', properties: { teamId: { type: 'string' }, limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: webhookOutputSchema } }; +export const createWebhookToolDefinition: MCPToolDefinition = { name: 'linear_createWebhook', annotations: additiveToolAnnotations(), description: 'Create a webhook for integration events', input_schema: { type: 'object', properties: { url: { type: 'string' }, resourceTypes: { type: 'array', items: { type: 'string' } }, teamId: { type: 'string' }, enabled: { type: 'boolean' }, label: { type: 'string' }, secret: { type: 'string' }, allPublicTeams: { type: 'boolean' } }, required: ['url', 'resourceTypes'] }, output_schema: webhookOutputSchema }; +export const deleteWebhookToolDefinition: MCPToolDefinition = { name: 'linear_deleteWebhook', annotations: destructiveToolAnnotations(), description: 'Delete a webhook', input_schema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, output_schema: { type: 'object', properties: { success: { type: 'boolean' }, id: { type: 'string' } } } }; +export const getAttachmentsToolDefinition: MCPToolDefinition = { name: 'linear_getAttachments', annotations: readOnlyToolAnnotations(), description: 'Get attachments for an issue', input_schema: { type: 'object', properties: { issueId: { type: 'string' }, limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } }, required: ['issueId'] }, output_schema: { type: 'array', items: attachmentOutputSchema } }; +export const addAttachmentToolDefinition: MCPToolDefinition = { name: 'linear_addAttachment', annotations: additiveToolAnnotations(), description: 'Add an attachment to an issue', input_schema: { type: 'object', properties: { issueId: { type: 'string' }, title: { type: 'string' }, url: { type: 'string' }, subtitle: { type: 'string' }, iconUrl: { type: 'string' }, metadata: { type: 'object', additionalProperties: true }, commentBody: { type: 'string' }, groupBySource: { type: 'boolean' } }, required: ['issueId', 'title', 'url'] }, output_schema: attachmentOutputSchema }; +export const getNotificationsToolDefinition: MCPToolDefinition = { name: 'linear_getNotifications', annotations: readOnlyToolAnnotations(), description: 'Get notifications for the current user', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: notificationOutputSchema } }; +export const markNotificationAsReadToolDefinition: MCPToolDefinition = { name: 'linear_markNotificationAsRead', annotations: idempotentMutationToolAnnotations(), description: 'Mark a notification as read', input_schema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] }, output_schema: { type: 'object', properties: { success: { type: 'boolean' }, id: { type: 'string' } } } }; +export const getSubscriptionsToolDefinition: MCPToolDefinition = { name: 'linear_getSubscriptions', annotations: readOnlyToolAnnotations(), description: 'Get subscriptions for the current user', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: subscriptionOutputSchema } }; +export const markAllNotificationsAsReadToolDefinition: MCPToolDefinition = { name: 'linear_markAllNotificationsAsRead', annotations: idempotentMutationToolAnnotations(), description: 'Mark all notifications as read', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema } } }, output_schema: { type: 'object', properties: { success: { type: 'boolean' }, count: { type: 'number' } } } }; +export const getUnreadNotificationCountToolDefinition: MCPToolDefinition = { name: 'linear_getUnreadNotificationCount', annotations: readOnlyToolAnnotations(), description: 'Get count of unread notifications', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema } } }, output_schema: { type: 'object', properties: { count: { type: 'number' } } } }; +export const getAuthenticationSessionsToolDefinition: MCPToolDefinition = { name: 'linear_getAuthenticationSessions', annotations: readOnlyToolAnnotations(), description: 'Get active authentication sessions', input_schema: { type: 'object', properties: {} }, output_schema: { type: 'array', items: authSessionOutputSchema } }; +export const logoutSessionToolDefinition: MCPToolDefinition = { name: 'linear_logoutSession', annotations: destructiveToolAnnotations(), description: 'Revoke a specific session', input_schema: { type: 'object', properties: { sessionId: { type: 'string' } }, required: ['sessionId'] }, output_schema: { type: 'object', properties: { success: { type: 'boolean' } } } }; +export const logoutOtherSessionsToolDefinition: MCPToolDefinition = { name: 'linear_logoutOtherSessions', annotations: destructiveToolAnnotations(), description: 'Revoke all other sessions', input_schema: { type: 'object', properties: {} }, output_schema: { type: 'object', properties: { success: { type: 'boolean' } } } }; +export const logoutAllSessionsToolDefinition: MCPToolDefinition = { name: 'linear_logoutAllSessions', annotations: destructiveToolAnnotations(), description: 'Revoke all sessions', input_schema: { type: 'object', properties: {} }, output_schema: { type: 'object', properties: { success: { type: 'boolean' } } } }; +export const getOrganizationAuditEventsToolDefinition: MCPToolDefinition = { name: 'linear_getOrganizationAuditEvents', annotations: readOnlyToolAnnotations(), description: 'Get audit events for the organization', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: auditEntryOutputSchema } }; +export const getUserAuditEventsToolDefinition: MCPToolDefinition = { name: 'linear_getUserAuditEvents', annotations: readOnlyToolAnnotations(), description: 'Get audit events for a specific user', input_schema: { type: 'object', properties: { userId: { type: 'string' }, limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } }, required: ['userId'] }, output_schema: { type: 'array', items: auditEntryOutputSchema } }; +export const getIntegrationsToolDefinition: MCPToolDefinition = { name: 'linear_getIntegrations', annotations: readOnlyToolAnnotations(), description: 'Get a list of active integrations', input_schema: { type: 'object', properties: { limit: { ...positiveLimitSchema }, includeArchived: { type: 'boolean' }, orderBy: { ...paginationOrderBySchema } } }, output_schema: { type: 'array', items: integrationOutputSchema } }; diff --git a/src/tools/definitions/project-tools.ts b/src/tools/definitions/project-tools.ts index 614383f..9c1805e 100644 --- a/src/tools/definitions/project-tools.ts +++ b/src/tools/definitions/project-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const issueOrderBySchema = { type: 'string', @@ -110,6 +116,7 @@ const projectOutputSchema = { */ export const getProjectsToolDefinition: MCPToolDefinition = { name: 'linear_getProjects', + annotations: readOnlyToolAnnotations(), description: 'Get a list of projects from Linear', input_schema: { type: 'object', @@ -123,6 +130,7 @@ export const getProjectsToolDefinition: MCPToolDefinition = { export const getProjectByIdToolDefinition: MCPToolDefinition = { name: 'linear_getProjectById', + annotations: readOnlyToolAnnotations(), description: 'Get details for a specific Linear project by ID', input_schema: { type: 'object', @@ -136,15 +144,22 @@ export const getProjectByIdToolDefinition: MCPToolDefinition = { export const getProjectMembersToolDefinition: MCPToolDefinition = { name: 'linear_getProjectMembers', + annotations: readOnlyToolAnnotations(), description: 'Get members assigned to a project', input_schema: { type: 'object', properties: { projectId: { type: 'string', description: 'ID of the project to inspect' }, - limit: { ...positiveLimitSchema, description: 'Maximum number of members to return (default: 25)' }, + limit: { + ...positiveLimitSchema, + description: 'Maximum number of members to return (default: 25)', + }, includeArchived: { type: 'boolean', description: 'Include archived members' }, includeDisabled: { type: 'boolean', description: 'Include disabled users' }, - orderBy: { ...issueOrderBySchema, description: 'Sort members by created or updated date when supported' }, + orderBy: { + ...issueOrderBySchema, + description: 'Sort members by created or updated date when supported', + }, }, required: ['projectId'], }, @@ -164,6 +179,7 @@ export const getProjectMembersToolDefinition: MCPToolDefinition = { export const addProjectMemberToolDefinition: MCPToolDefinition = { name: 'linear_addProjectMember', + annotations: idempotentMutationToolAnnotations(), description: 'Add a member to a project', input_schema: { type: 'object', @@ -185,6 +201,7 @@ export const addProjectMemberToolDefinition: MCPToolDefinition = { export const removeProjectMemberToolDefinition: MCPToolDefinition = { name: 'linear_removeProjectMember', + annotations: idempotentMutationToolAnnotations(), description: 'Remove a member from a project', input_schema: { type: 'object', @@ -209,6 +226,7 @@ export const removeProjectMemberToolDefinition: MCPToolDefinition = { */ export const createProjectToolDefinition: MCPToolDefinition = { name: 'linear_createProject', + annotations: additiveToolAnnotations(), description: 'Create a new project in Linear', input_schema: { type: 'object', @@ -277,6 +295,7 @@ export const createProjectToolDefinition: MCPToolDefinition = { */ export const updateProjectToolDefinition: MCPToolDefinition = { name: 'linear_updateProject', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing project in Linear', input_schema: { type: 'object', @@ -387,6 +406,7 @@ const archiveProjectUpdateOutputSchema = { */ export const createProjectUpdateToolDefinition: MCPToolDefinition = { name: 'linear_createProjectUpdate', + annotations: additiveToolAnnotations(), description: 'Create a new project update for a Linear project', input_schema: { type: 'object', @@ -419,6 +439,7 @@ export const createProjectUpdateToolDefinition: MCPToolDefinition = { */ export const updateProjectUpdateToolDefinition: MCPToolDefinition = { name: 'linear_updateProjectUpdate', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing project update in Linear', input_schema: { type: 'object', @@ -448,6 +469,7 @@ export const updateProjectUpdateToolDefinition: MCPToolDefinition = { export const getProjectUpdateByIdToolDefinition: MCPToolDefinition = { name: 'linear_getProjectUpdateById', + annotations: readOnlyToolAnnotations(), description: 'Get a specific Linear project update by ID', input_schema: { type: 'object', @@ -464,6 +486,7 @@ export const getProjectUpdateByIdToolDefinition: MCPToolDefinition = { export const archiveProjectUpdateToolDefinition: MCPToolDefinition = { name: 'linear_archiveProjectUpdate', + annotations: destructiveToolAnnotations(), description: 'Archive a Linear project update', input_schema: { type: 'object', @@ -477,6 +500,7 @@ export const archiveProjectUpdateToolDefinition: MCPToolDefinition = { export const unarchiveProjectUpdateToolDefinition: MCPToolDefinition = { name: 'linear_unarchiveProjectUpdate', + annotations: idempotentMutationToolAnnotations(), description: 'Restore an archived Linear project update', input_schema: { type: 'object', @@ -490,6 +514,7 @@ export const unarchiveProjectUpdateToolDefinition: MCPToolDefinition = { export const deleteProjectUpdateToolDefinition: MCPToolDefinition = { name: 'linear_deleteProjectUpdate', + annotations: destructiveToolAnnotations(), description: 'Delete a Linear project update', input_schema: { type: 'object', @@ -506,6 +531,7 @@ export const deleteProjectUpdateToolDefinition: MCPToolDefinition = { */ export const getProjectUpdatesToolDefinition: MCPToolDefinition = { name: 'linear_getProjectUpdates', + annotations: readOnlyToolAnnotations(), description: 'Get all updates associated with a project', input_schema: { type: 'object', @@ -532,6 +558,7 @@ export const getProjectUpdatesToolDefinition: MCPToolDefinition = { */ export const archiveProjectToolDefinition: MCPToolDefinition = { name: 'linear_archiveProject', + annotations: destructiveToolAnnotations(), description: 'Archive a project', input_schema: { type: 'object', @@ -565,6 +592,7 @@ export const archiveProjectToolDefinition: MCPToolDefinition = { */ export const addIssueToProjectToolDefinition: MCPToolDefinition = { name: 'linear_addIssueToProject', + annotations: idempotentMutationToolAnnotations(), description: 'Add an existing issue to a project', input_schema: { type: 'object', @@ -608,6 +636,7 @@ export const addIssueToProjectToolDefinition: MCPToolDefinition = { */ export const removeIssueFromProjectToolDefinition: MCPToolDefinition = { name: 'linear_removeIssueFromProject', + annotations: idempotentMutationToolAnnotations(), description: 'Remove an existing issue from a project', input_schema: { type: 'object', @@ -651,7 +680,9 @@ export const removeIssueFromProjectToolDefinition: MCPToolDefinition = { */ export const getProjectIssuesToolDefinition: MCPToolDefinition = { name: 'linear_getProjectIssues', - description: 'Get issues associated with a project, with PM-friendly filters like state, assignee, labels, cycle, and milestone', + annotations: readOnlyToolAnnotations(), + description: + 'Get issues associated with a project, with PM-friendly filters like state, assignee, labels, cycle, and milestone', input_schema: { type: 'object', properties: { diff --git a/src/tools/definitions/release-tools.ts b/src/tools/definitions/release-tools.ts index c12feac..85c22ce 100644 --- a/src/tools/definitions/release-tools.ts +++ b/src/tools/definitions/release-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const paginationSchema = { type: 'integer', @@ -199,6 +205,7 @@ const releasePipelineDetailSchema = { export const getReleasePipelinesToolDefinition: MCPToolDefinition = { name: 'linear_getReleasePipelines', + annotations: readOnlyToolAnnotations(), description: 'Get release pipelines from Linear', input_schema: { type: 'object', @@ -225,6 +232,7 @@ export const getReleasePipelinesToolDefinition: MCPToolDefinition = { export const getReleasePipelineByIdToolDefinition: MCPToolDefinition = { name: 'linear_getReleasePipelineById', + annotations: readOnlyToolAnnotations(), description: 'Get details of a specific release pipeline', input_schema: { type: 'object', @@ -241,6 +249,7 @@ export const getReleasePipelineByIdToolDefinition: MCPToolDefinition = { export const createReleasePipelineToolDefinition: MCPToolDefinition = { name: 'linear_createReleasePipeline', + annotations: additiveToolAnnotations(), description: 'Create a new release pipeline', input_schema: { type: 'object', @@ -279,6 +288,7 @@ export const createReleasePipelineToolDefinition: MCPToolDefinition = { export const updateReleasePipelineToolDefinition: MCPToolDefinition = { name: 'linear_updateReleasePipeline', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing release pipeline. Provide id plus at least one other field to change.', input_schema: { @@ -322,6 +332,7 @@ export const updateReleasePipelineToolDefinition: MCPToolDefinition = { export const archiveReleasePipelineToolDefinition: MCPToolDefinition = { name: 'linear_archiveReleasePipeline', + annotations: destructiveToolAnnotations(), description: 'Archive a release pipeline', input_schema: { type: 'object', @@ -344,6 +355,7 @@ export const archiveReleasePipelineToolDefinition: MCPToolDefinition = { export const unarchiveReleasePipelineToolDefinition: MCPToolDefinition = { name: 'linear_unarchiveReleasePipeline', + annotations: idempotentMutationToolAnnotations(), description: 'Unarchive a release pipeline', input_schema: { type: 'object', @@ -366,6 +378,7 @@ export const unarchiveReleasePipelineToolDefinition: MCPToolDefinition = { export const deleteReleasePipelineToolDefinition: MCPToolDefinition = { name: 'linear_deleteReleasePipeline', + annotations: destructiveToolAnnotations(), description: 'Delete a release pipeline', input_schema: { type: 'object', @@ -388,6 +401,7 @@ export const deleteReleasePipelineToolDefinition: MCPToolDefinition = { export const getReleasesToolDefinition: MCPToolDefinition = { name: 'linear_getReleases', + annotations: readOnlyToolAnnotations(), description: 'Get releases from Linear with optional pipeline and stage filters', input_schema: { type: 'object', @@ -422,6 +436,7 @@ export const getReleasesToolDefinition: MCPToolDefinition = { export const getReleaseByIdToolDefinition: MCPToolDefinition = { name: 'linear_getReleaseById', + annotations: readOnlyToolAnnotations(), description: 'Get details of a specific release', input_schema: { type: 'object', @@ -451,6 +466,7 @@ export const getReleaseByIdToolDefinition: MCPToolDefinition = { export const searchReleasesToolDefinition: MCPToolDefinition = { name: 'linear_searchReleases', + annotations: readOnlyToolAnnotations(), description: 'Search releases by term, version, or pipeline name', input_schema: { type: 'object', @@ -485,6 +501,7 @@ export const searchReleasesToolDefinition: MCPToolDefinition = { export const getReleaseStagesToolDefinition: MCPToolDefinition = { name: 'linear_getReleaseStages', + annotations: readOnlyToolAnnotations(), description: 'Get release stages from Linear', input_schema: { type: 'object', @@ -515,6 +532,7 @@ export const getReleaseStagesToolDefinition: MCPToolDefinition = { export const createReleaseStageToolDefinition: MCPToolDefinition = { name: 'linear_createReleaseStage', + annotations: additiveToolAnnotations(), description: 'Create a new release stage in a pipeline', input_schema: { type: 'object', @@ -555,6 +573,7 @@ export const createReleaseStageToolDefinition: MCPToolDefinition = { export const updateReleaseStageToolDefinition: MCPToolDefinition = { name: 'linear_updateReleaseStage', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing release stage. Provide id plus at least one other field to change.', input_schema: { @@ -588,6 +607,7 @@ export const updateReleaseStageToolDefinition: MCPToolDefinition = { export const archiveReleaseStageToolDefinition: MCPToolDefinition = { name: 'linear_archiveReleaseStage', + annotations: destructiveToolAnnotations(), description: 'Archive a release stage', input_schema: { type: 'object', @@ -610,6 +630,7 @@ export const archiveReleaseStageToolDefinition: MCPToolDefinition = { export const unarchiveReleaseStageToolDefinition: MCPToolDefinition = { name: 'linear_unarchiveReleaseStage', + annotations: idempotentMutationToolAnnotations(), description: 'Unarchive a release stage', input_schema: { type: 'object', @@ -632,6 +653,7 @@ export const unarchiveReleaseStageToolDefinition: MCPToolDefinition = { export const getReleaseNotesToolDefinition: MCPToolDefinition = { name: 'linear_getReleaseNotes', + annotations: readOnlyToolAnnotations(), description: 'Get release notes from Linear', input_schema: { type: 'object', @@ -658,6 +680,7 @@ export const getReleaseNotesToolDefinition: MCPToolDefinition = { export const getReleaseNoteByIdToolDefinition: MCPToolDefinition = { name: 'linear_getReleaseNoteById', + annotations: readOnlyToolAnnotations(), description: 'Get details of a specific release note', input_schema: { type: 'object', @@ -677,6 +700,7 @@ export const getReleaseNoteByIdToolDefinition: MCPToolDefinition = { export const createReleaseToolDefinition: MCPToolDefinition = { name: 'linear_createRelease', + annotations: additiveToolAnnotations(), description: 'Create a new release in a release pipeline', input_schema: { type: 'object', @@ -733,6 +757,7 @@ export const createReleaseToolDefinition: MCPToolDefinition = { export const updateReleaseToolDefinition: MCPToolDefinition = { name: 'linear_updateRelease', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing release. Provide id plus at least one other field to change.', input_schema: { type: 'object', @@ -793,6 +818,7 @@ export const updateReleaseToolDefinition: MCPToolDefinition = { export const completeReleaseToolDefinition: MCPToolDefinition = { name: 'linear_completeRelease', + annotations: idempotentMutationToolAnnotations(), description: 'Mark a release as completed within a pipeline. The target pipeline must already have a completed release stage configured.', input_schema: { @@ -814,6 +840,7 @@ export const completeReleaseToolDefinition: MCPToolDefinition = { export const archiveReleaseToolDefinition: MCPToolDefinition = { name: 'linear_archiveRelease', + annotations: destructiveToolAnnotations(), description: 'Archive a release', input_schema: { type: 'object', @@ -836,6 +863,7 @@ export const archiveReleaseToolDefinition: MCPToolDefinition = { export const unarchiveReleaseToolDefinition: MCPToolDefinition = { name: 'linear_unarchiveRelease', + annotations: idempotentMutationToolAnnotations(), description: 'Unarchive a release', input_schema: { type: 'object', @@ -858,6 +886,7 @@ export const unarchiveReleaseToolDefinition: MCPToolDefinition = { export const addIssueToReleaseToolDefinition: MCPToolDefinition = { name: 'linear_addIssueToRelease', + annotations: idempotentMutationToolAnnotations(), description: 'Add an issue to a release', input_schema: { type: 'object', @@ -885,6 +914,7 @@ export const addIssueToReleaseToolDefinition: MCPToolDefinition = { export const removeIssueFromReleaseToolDefinition: MCPToolDefinition = { name: 'linear_removeIssueFromRelease', + annotations: idempotentMutationToolAnnotations(), description: 'Remove an issue from a release', input_schema: { type: 'object', @@ -912,6 +942,7 @@ export const removeIssueFromReleaseToolDefinition: MCPToolDefinition = { export const createReleaseNoteToolDefinition: MCPToolDefinition = { name: 'linear_createReleaseNote', + annotations: additiveToolAnnotations(), description: 'Create a release note using either explicit release IDs or a release range within a pipeline', input_schema: { @@ -946,6 +977,7 @@ export const createReleaseNoteToolDefinition: MCPToolDefinition = { export const updateReleaseNoteToolDefinition: MCPToolDefinition = { name: 'linear_updateReleaseNote', + annotations: idempotentMutationToolAnnotations(), description: 'Update a release note using either explicit release IDs or a release range. Provide id plus at least one other field to change.', input_schema: { @@ -980,6 +1012,7 @@ export const updateReleaseNoteToolDefinition: MCPToolDefinition = { export const deleteReleaseNoteToolDefinition: MCPToolDefinition = { name: 'linear_deleteReleaseNote', + annotations: destructiveToolAnnotations(), description: 'Delete a release note', input_schema: { type: 'object', diff --git a/src/tools/definitions/roadmap-tools.ts b/src/tools/definitions/roadmap-tools.ts index 3a3de1c..d6ece0a 100644 --- a/src/tools/definitions/roadmap-tools.ts +++ b/src/tools/definitions/roadmap-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const roadmapUserSchema = { type: ['object', 'null'], @@ -36,6 +42,7 @@ const roadmapProperties = { export const getRoadmapsToolDefinition: MCPToolDefinition = { name: 'linear_getRoadmaps', + annotations: readOnlyToolAnnotations(), description: 'Get a list of roadmaps from Linear', input_schema: { type: 'object', @@ -68,6 +75,7 @@ export const getRoadmapsToolDefinition: MCPToolDefinition = { export const getRoadmapByIdToolDefinition: MCPToolDefinition = { name: 'linear_getRoadmapById', + annotations: readOnlyToolAnnotations(), description: 'Get details of a specific roadmap by ID', input_schema: { type: 'object', @@ -93,6 +101,7 @@ export const getRoadmapByIdToolDefinition: MCPToolDefinition = { export const createRoadmapToolDefinition: MCPToolDefinition = { name: 'linear_createRoadmap', + annotations: additiveToolAnnotations(), description: 'Create a new roadmap in Linear', input_schema: { type: 'object', @@ -134,6 +143,7 @@ export const createRoadmapToolDefinition: MCPToolDefinition = { export const updateRoadmapToolDefinition: MCPToolDefinition = { name: 'linear_updateRoadmap', + annotations: idempotentMutationToolAnnotations(), description: 'Update an existing roadmap', input_schema: { type: 'object', @@ -179,6 +189,7 @@ export const updateRoadmapToolDefinition: MCPToolDefinition = { export const archiveRoadmapToolDefinition: MCPToolDefinition = { name: 'linear_archiveRoadmap', + annotations: destructiveToolAnnotations(), description: 'Archive a roadmap', input_schema: { type: 'object', diff --git a/src/tools/definitions/server-tools.ts b/src/tools/definitions/server-tools.ts index 28c4051..1824c5e 100644 --- a/src/tools/definitions/server-tools.ts +++ b/src/tools/definitions/server-tools.ts @@ -1,7 +1,9 @@ import { MCPToolDefinition } from '../../types.js'; +import { readOnlyToolAnnotations } from '../../tool-annotations.js'; export const getRateLimitStatusToolDefinition: MCPToolDefinition = { name: 'linear_getRateLimitStatus', + annotations: readOnlyToolAnnotations(), description: 'Get the shared MCP-side Linear rate-limit cooldown and retry snapshot', input_schema: { type: 'object', @@ -21,6 +23,7 @@ export const getRateLimitStatusToolDefinition: MCPToolDefinition = { export const getServerStatusToolDefinition: MCPToolDefinition = { name: 'linear_getServerStatus', + annotations: readOnlyToolAnnotations(), description: 'Get MCP Linear server runtime status, counts, and current rate-limit snapshot', input_schema: { type: 'object', diff --git a/src/tools/definitions/team-tools.ts b/src/tools/definitions/team-tools.ts index a24cc40..b8c3bec 100644 --- a/src/tools/definitions/team-tools.ts +++ b/src/tools/definitions/team-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const positiveLimitSchema = { type: 'integer', minimum: 1 }; const paginationOrderBySchema = { type: 'string', enum: ['createdAt', 'updatedAt'] }; @@ -63,6 +69,7 @@ const teamLabelOutputSchema = { */ export const getTeamsToolDefinition: MCPToolDefinition = { name: 'linear_getTeams', + annotations: readOnlyToolAnnotations(), description: 'Get a list of teams from Linear', input_schema: { type: 'object', @@ -97,6 +104,7 @@ export const getTeamsToolDefinition: MCPToolDefinition = { */ export const getWorkflowStatesToolDefinition: MCPToolDefinition = { name: 'linear_getWorkflowStates', + annotations: readOnlyToolAnnotations(), description: 'Get workflow states for a team', input_schema: { type: 'object', @@ -130,6 +138,7 @@ export const getWorkflowStatesToolDefinition: MCPToolDefinition = { export const createWorkflowStateToolDefinition: MCPToolDefinition = { name: 'linear_createWorkflowState', + annotations: additiveToolAnnotations(), description: 'Create a new workflow state', input_schema: { type: 'object', @@ -148,6 +157,7 @@ export const createWorkflowStateToolDefinition: MCPToolDefinition = { export const updateWorkflowStateToolDefinition: MCPToolDefinition = { name: 'linear_updateWorkflowState', + annotations: idempotentMutationToolAnnotations(), description: 'Update a workflow state. Provide id plus at least one other field to change.', input_schema: { type: 'object', @@ -165,6 +175,7 @@ export const updateWorkflowStateToolDefinition: MCPToolDefinition = { export const updateTeamToolDefinition: MCPToolDefinition = { name: 'linear_updateTeam', + annotations: idempotentMutationToolAnnotations(), description: 'Update team settings', input_schema: { type: 'object', @@ -187,14 +198,21 @@ export const updateTeamToolDefinition: MCPToolDefinition = { export const getTeamMembershipsToolDefinition: MCPToolDefinition = { name: 'linear_getTeamMemberships', + annotations: readOnlyToolAnnotations(), description: 'Get team memberships', input_schema: { type: 'object', properties: { teamId: { type: 'string', description: 'ID of the team to inspect' }, - limit: { ...positiveLimitSchema, description: 'Maximum number of memberships to return (default: 25)' }, + limit: { + ...positiveLimitSchema, + description: 'Maximum number of memberships to return (default: 25)', + }, includeArchived: { type: 'boolean', description: 'Include archived memberships' }, - orderBy: { ...paginationOrderBySchema, description: 'Sort memberships by created or updated date' }, + orderBy: { + ...paginationOrderBySchema, + description: 'Sort memberships by created or updated date', + }, }, required: ['teamId'], }, @@ -203,6 +221,7 @@ export const getTeamMembershipsToolDefinition: MCPToolDefinition = { export const createTeamToolDefinition: MCPToolDefinition = { name: 'linear_createTeam', + annotations: additiveToolAnnotations(), description: 'Create a new team', input_schema: { type: 'object', @@ -224,13 +243,22 @@ export const createTeamToolDefinition: MCPToolDefinition = { export const archiveTeamToolDefinition: MCPToolDefinition = { name: 'linear_archiveTeam', + annotations: destructiveToolAnnotations(), description: 'Archive a team', - input_schema: { type: 'object', properties: { id: { type: 'string', description: 'ID of the team to archive' } }, required: ['id'] }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, id: { type: 'string' } } }, + input_schema: { + type: 'object', + properties: { id: { type: 'string', description: 'ID of the team to archive' } }, + required: ['id'], + }, + output_schema: { + type: 'object', + properties: { success: { type: 'boolean' }, id: { type: 'string' } }, + }, }; export const addUserToTeamToolDefinition: MCPToolDefinition = { name: 'linear_addUserToTeam', + annotations: idempotentMutationToolAnnotations(), description: 'Add a user to a team', input_schema: { type: 'object', @@ -247,22 +275,35 @@ export const addUserToTeamToolDefinition: MCPToolDefinition = { export const removeUserFromTeamToolDefinition: MCPToolDefinition = { name: 'linear_removeUserFromTeam', + annotations: idempotentMutationToolAnnotations(), description: 'Remove a user from a team', input_schema: { type: 'object', properties: { teamId: { type: 'string', description: 'ID of the team to update' }, userId: { type: 'string', description: 'ID of the user to remove' }, - alsoLeaveParentTeams: { type: 'boolean', description: 'Also leave parent teams when removing membership' }, + alsoLeaveParentTeams: { + type: 'boolean', + description: 'Also leave parent teams when removing membership', + }, }, required: ['teamId', 'userId'], }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, teamId: { type: 'string' }, userId: { type: 'string' } } }, + output_schema: { + type: 'object', + properties: { + success: { type: 'boolean' }, + teamId: { type: 'string' }, + userId: { type: 'string' }, + }, + }, }; export const updateTeamMembershipToolDefinition: MCPToolDefinition = { name: 'linear_updateTeamMembership', - description: 'Update a user\'s role in a team. Provide id plus at least one other field to change.', + annotations: idempotentMutationToolAnnotations(), + description: + "Update a user's role in a team. Provide id plus at least one other field to change.", input_schema: { type: 'object', properties: { @@ -277,14 +318,21 @@ export const updateTeamMembershipToolDefinition: MCPToolDefinition = { export const getTeamLabelsToolDefinition: MCPToolDefinition = { name: 'linear_getTeamLabels', + annotations: readOnlyToolAnnotations(), description: 'Get labels for a specific team', input_schema: { type: 'object', properties: { teamId: { type: 'string', description: 'ID of the team to inspect' }, - limit: { ...positiveLimitSchema, description: 'Maximum number of labels to return (default: 25)' }, + limit: { + ...positiveLimitSchema, + description: 'Maximum number of labels to return (default: 25)', + }, includeArchived: { type: 'boolean', description: 'Include archived labels' }, - orderBy: { ...paginationOrderBySchema, description: 'Sort labels by created or updated date' }, + orderBy: { + ...paginationOrderBySchema, + description: 'Sort labels by created or updated date', + }, }, required: ['teamId'], }, @@ -293,6 +341,7 @@ export const getTeamLabelsToolDefinition: MCPToolDefinition = { export const createTeamLabelToolDefinition: MCPToolDefinition = { name: 'linear_createTeamLabel', + annotations: additiveToolAnnotations(), description: 'Create a new label for a team', input_schema: { type: 'object', diff --git a/src/tools/definitions/template-tools.ts b/src/tools/definitions/template-tools.ts index d80e6d9..e7e8b1c 100644 --- a/src/tools/definitions/template-tools.ts +++ b/src/tools/definitions/template-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; const positiveLimitSchema = { type: 'integer', @@ -29,13 +35,20 @@ const templateOutputSchema = { export const getIssueTemplatesToolDefinition: MCPToolDefinition = { name: 'linear_getIssueTemplates', + annotations: readOnlyToolAnnotations(), description: 'Get a list of issue templates', input_schema: { type: 'object', properties: { - limit: { ...positiveLimitSchema, description: 'Maximum number of templates to return (default: 25)' }, + limit: { + ...positiveLimitSchema, + description: 'Maximum number of templates to return (default: 25)', + }, includeArchived: { type: 'boolean', description: 'Include archived templates' }, - orderBy: { ...paginationOrderBySchema, description: 'Sort templates by created or updated date' }, + orderBy: { + ...paginationOrderBySchema, + description: 'Sort templates by created or updated date', + }, }, }, output_schema: { type: 'array', items: templateOutputSchema }, @@ -43,13 +56,19 @@ export const getIssueTemplatesToolDefinition: MCPToolDefinition = { export const getIssueTemplateByIdToolDefinition: MCPToolDefinition = { name: 'linear_getIssueTemplateById', + annotations: readOnlyToolAnnotations(), description: 'Get details of a specific issue template', - input_schema: { type: 'object', properties: { id: { type: 'string', description: 'ID of the template to retrieve' } }, required: ['id'] }, + input_schema: { + type: 'object', + properties: { id: { type: 'string', description: 'ID of the template to retrieve' } }, + required: ['id'], + }, output_schema: templateOutputSchema, }; export const createIssueTemplateToolDefinition: MCPToolDefinition = { name: 'linear_createIssueTemplate', + annotations: additiveToolAnnotations(), description: 'Create a new issue template', input_schema: { type: 'object', @@ -57,7 +76,11 @@ export const createIssueTemplateToolDefinition: MCPToolDefinition = { name: { type: 'string', description: 'Template name' }, description: { type: 'string', description: 'Optional template description' }, teamId: { type: 'string', description: 'Optional team ID' }, - templateData: { type: 'object', additionalProperties: true, description: 'Template issue payload as JSON' }, + templateData: { + type: 'object', + additionalProperties: true, + description: 'Template issue payload as JSON', + }, sortOrder: { type: 'number', description: 'Optional sort order' }, }, required: ['name', 'templateData'], @@ -67,7 +90,9 @@ export const createIssueTemplateToolDefinition: MCPToolDefinition = { export const updateIssueTemplateToolDefinition: MCPToolDefinition = { name: 'linear_updateIssueTemplate', - description: 'Update an existing issue template. Provide id plus at least one other field to change.', + annotations: idempotentMutationToolAnnotations(), + description: + 'Update an existing issue template. Provide id plus at least one other field to change.', input_schema: { type: 'object', properties: { @@ -85,6 +110,7 @@ export const updateIssueTemplateToolDefinition: MCPToolDefinition = { export const createIssueFromTemplateToolDefinition: MCPToolDefinition = { name: 'linear_createIssueFromTemplate', + annotations: additiveToolAnnotations(), description: 'Create a new issue from a template', input_schema: { type: 'object', @@ -112,14 +138,21 @@ export const createIssueFromTemplateToolDefinition: MCPToolDefinition = { export const getTeamTemplatesToolDefinition: MCPToolDefinition = { name: 'linear_getTeamTemplates', + annotations: readOnlyToolAnnotations(), description: 'Get templates for a specific team', input_schema: { type: 'object', properties: { teamId: { type: 'string', description: 'ID of the team to inspect' }, - limit: { ...positiveLimitSchema, description: 'Maximum number of templates to return (default: 25)' }, + limit: { + ...positiveLimitSchema, + description: 'Maximum number of templates to return (default: 25)', + }, includeArchived: { type: 'boolean', description: 'Include archived templates' }, - orderBy: { ...paginationOrderBySchema, description: 'Sort templates by created or updated date' }, + orderBy: { + ...paginationOrderBySchema, + description: 'Sort templates by created or updated date', + }, }, required: ['teamId'], }, @@ -128,7 +161,15 @@ export const getTeamTemplatesToolDefinition: MCPToolDefinition = { export const archiveTemplateToolDefinition: MCPToolDefinition = { name: 'linear_archiveTemplate', + annotations: destructiveToolAnnotations(), description: 'Archive an issue template', - input_schema: { type: 'object', properties: { id: { type: 'string', description: 'ID of the template to archive' } }, required: ['id'] }, - output_schema: { type: 'object', properties: { success: { type: 'boolean' }, id: { type: 'string' } } }, + input_schema: { + type: 'object', + properties: { id: { type: 'string', description: 'ID of the template to archive' } }, + required: ['id'], + }, + output_schema: { + type: 'object', + properties: { success: { type: 'boolean' }, id: { type: 'string' } }, + }, }; diff --git a/src/tools/definitions/user-tools.ts b/src/tools/definitions/user-tools.ts index 76172ac..34c1364 100644 --- a/src/tools/definitions/user-tools.ts +++ b/src/tools/definitions/user-tools.ts @@ -1,10 +1,12 @@ import { MCPToolDefinition } from '../../types.js'; +import { readOnlyToolAnnotations } from '../../tool-annotations.js'; /** * Tool definition for getting the current viewer's information */ export const getViewerToolDefinition: MCPToolDefinition = { name: 'linear_getViewer', + annotations: readOnlyToolAnnotations(), description: 'Get information about the currently authenticated user', input_schema: { type: 'object', @@ -34,6 +36,7 @@ export const getViewerToolDefinition: MCPToolDefinition = { */ export const getOrganizationToolDefinition: MCPToolDefinition = { name: 'linear_getOrganization', + annotations: readOnlyToolAnnotations(), description: 'Get information about the current Linear organization', input_schema: { type: 'object', @@ -55,6 +58,7 @@ export const getOrganizationToolDefinition: MCPToolDefinition = { */ export const getUsersToolDefinition: MCPToolDefinition = { name: 'linear_getUsers', + annotations: readOnlyToolAnnotations(), description: 'Get a list of users in the Linear organization', input_schema: { type: 'object', @@ -80,6 +84,7 @@ export const getUsersToolDefinition: MCPToolDefinition = { */ export const getLabelsToolDefinition: MCPToolDefinition = { name: 'linear_getLabels', + annotations: readOnlyToolAnnotations(), description: 'Get a list of issue labels from Linear', input_schema: { type: 'object', diff --git a/src/tools/definitions/view-tools.ts b/src/tools/definitions/view-tools.ts index 70eef80..9b3fdea 100644 --- a/src/tools/definitions/view-tools.ts +++ b/src/tools/definitions/view-tools.ts @@ -1,4 +1,10 @@ import { MCPToolDefinition } from '../../types.js'; +import { + additiveToolAnnotations, + destructiveToolAnnotations, + idempotentMutationToolAnnotations, + readOnlyToolAnnotations, +} from '../../tool-annotations.js'; // Linear calls these saved views in the UI, but the GraphQL API and SDK expose them as CustomView. @@ -61,6 +67,7 @@ const savedViewOutputItemSchema = { export const getSavedViewsToolDefinition: MCPToolDefinition = { name: 'linear_getSavedViews', + annotations: readOnlyToolAnnotations(), description: 'Get Linear saved views (API: CustomView)', input_schema: { type: 'object', @@ -86,6 +93,7 @@ export const getSavedViewsToolDefinition: MCPToolDefinition = { export const createSavedViewToolDefinition: MCPToolDefinition = { name: 'linear_createSavedView', + annotations: additiveToolAnnotations(), description: 'Create a Linear saved view (API: createCustomView)', input_schema: { type: 'object', @@ -142,6 +150,7 @@ export const createSavedViewToolDefinition: MCPToolDefinition = { export const updateSavedViewToolDefinition: MCPToolDefinition = { name: 'linear_updateSavedView', + annotations: idempotentMutationToolAnnotations(), description: 'Update a Linear saved view (API: updateCustomView). Provide id plus at least one other field to change.', input_schema: { @@ -185,7 +194,8 @@ export const updateSavedViewToolDefinition: MCPToolDefinition = { }, filters: { ...nullableObjectSchema, - description: 'Updated raw filters object to store on the saved view. Pass null to clear it.', + description: + 'Updated raw filters object to store on the saved view. Pass null to clear it.', }, filterData: { ...nullableObjectSchema, @@ -203,6 +213,7 @@ export const updateSavedViewToolDefinition: MCPToolDefinition = { export const deleteSavedViewToolDefinition: MCPToolDefinition = { name: 'linear_deleteSavedView', + annotations: destructiveToolAnnotations(), description: 'Delete a Linear saved view (API: deleteCustomView)', input_schema: { type: 'object', @@ -225,6 +236,7 @@ export const deleteSavedViewToolDefinition: MCPToolDefinition = { export const getFavoriteViewsToolDefinition: MCPToolDefinition = { name: 'linear_getFavoriteViews', + annotations: readOnlyToolAnnotations(), description: 'Get favorite Linear views, including saved views and predefined views', input_schema: { type: 'object', @@ -319,7 +331,8 @@ const favoriteMutationOutputSchema = { export const addToFavoritesToolDefinition: MCPToolDefinition = { name: 'linear_addToFavorites', - description: 'Add an entity to the current user\'s Linear favorites', + annotations: idempotentMutationToolAnnotations(), + description: "Add an entity to the current user's Linear favorites", input_schema: { type: 'object', properties: { @@ -335,8 +348,9 @@ export const addToFavoritesToolDefinition: MCPToolDefinition = { export const removeFromFavoritesToolDefinition: MCPToolDefinition = { name: 'linear_removeFromFavorites', + annotations: idempotentMutationToolAnnotations(), description: - 'Remove an entity or favorite entry from the current user\'s Linear favorites. Provide either favoriteId or entityId.', + "Remove an entity or favorite entry from the current user's Linear favorites. Provide either favoriteId or entityId.", input_schema: { type: 'object', properties: { diff --git a/src/types.ts b/src/types.ts index 26ef413..2c59e11 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,15 @@ +/** + * MCP tool behavior hints, advertised to clients so they can auto-allow safe + * reads and gate destructive calls. All four hints are required internally so + * every tool ships an explicit, reviewed classification. + */ +export interface ToolAnnotations { + readOnlyHint: boolean; + destructiveHint: boolean; + idempotentHint: boolean; + openWorldHint: boolean; +} + /** * Interface for MCP Tool Definition in our format */ @@ -15,4 +27,5 @@ export interface MCPToolDefinition { properties?: Record; items?: any; }; + annotations: ToolAnnotations; }