From 0af955f79de1684f43e98f95285baa9cd40a232a Mon Sep 17 00:00:00 2001 From: Tobias Strebitzer Date: Tue, 16 Jun 2026 12:13:15 +0800 Subject: [PATCH 1/5] feat(mcp): add opt-in MCP server via additive @Mcp() decorators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose existing NestJS controllers as Model Context Protocol (MCP) tools so AI agents can drive WhatsApp as first-class tools — without replacing the controllers or taking a core framework dependency. The approach is purely additive: each tool-shaped route opts in with a single @Mcp() decorator from @silkweave/nestjs. Controllers, REST paths/ verbs, DTOs, services, guards, and the dashboard are all unchanged. MCP is off by default and only loaded when MCP_ENABLED=true, mirroring the existing QUEUE_ENABLED lazy-require pattern in app.module.ts. - 114 routes across the existing controllers decorated with @Mcp() (REST untouched). - app.module.ts: opt-in SilkweaveModule.forRoot() mounts the MCP transport at /mcp and runs the global ApiKeyGuard on tool calls via globalGuards. - Auth fully enforced over MCP: @RequireRole and per-key allowedSessions scoping both apply; throttler intentionally excluded. - Skipped non-tool-shaped routes: Prometheus metrics text and the login-only POST /api/auth/validate. - README: new "MCP" section (enablement, /mcp, auth model, additive nature). - jest: moduleNameMapper stub for the ESM-only @silkweave/nestjs so the three controller specs that import their controllers still load under the CommonJS test runner. Verified: build, lint, and unit tests green; booting with MCP_ENABLED registers 114 tools; unauthenticated tool calls are rejected. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 17 + package-lock.json | 487 ++++++++++++++++-- package.json | 5 + src/app.module.ts | 20 + src/modules/audit/audit.controller.ts | 2 + src/modules/auth/auth.controller.ts | 7 + src/modules/catalog/catalog.controller.ts | 6 + src/modules/channel/channel.controller.ts | 6 + src/modules/contact/contact.controller.ts | 7 + src/modules/group/group.controller.ts | 13 + src/modules/health/health.controller.ts | 4 + src/modules/infra/infra.controller.ts | 13 + src/modules/label/label.controller.ts | 6 + src/modules/message/message.controller.ts | 20 + src/modules/plugins/plugins.controller.ts | 7 + src/modules/session/session.controller.ts | 12 + src/modules/settings/settings.controller.ts | 3 + src/modules/stats/stats.controller.ts | 4 + src/modules/status/status.controller.ts | 7 + src/modules/template/template.controller.ts | 6 + src/modules/webhook/webhook.controller.ts | 7 + .../webhook/webhooks-list.controller.ts | 2 + test/mocks/silkweave-nestjs.ts | 15 + 23 files changed, 623 insertions(+), 53 deletions(-) create mode 100644 test/mocks/silkweave-nestjs.ts diff --git a/README.md b/README.md index 8d09397e..836ce213 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,23 @@ curl -X POST http://localhost:2785/api/sessions/{sessionId}/webhooks \ --- +## 🤖 MCP (Model Context Protocol) + +OpenWA can expose its API to AI agents as **MCP tools**, additively and **off by default**. The existing REST API is unchanged — individual routes simply opt in, so every MCP tool maps 1:1 to an existing endpoint. + +Enable it by setting `MCP_ENABLED=true`. A Streamable-HTTP MCP endpoint then mounts at **`/mcp`**: + +```bash +MCP_ENABLED=true npm run start:dev +# MCP endpoint: http://localhost:2785/mcp +``` + +- **Auth is fully enforced.** Every tool call runs the same API-key guard as REST — pass your key via the `X-API-Key` header. Role requirements (`viewer`/`operator`/`admin`) and per-key session scoping apply exactly as they do over HTTP. (IP-allowlisted keys can't authorize MCP calls, since there's no client IP over MCP.) +- **What's exposed:** the JSON request/response operations across sessions, messages, groups, contacts, labels, channels, templates, webhooks, catalog, status, stats, plugins, infra and auth (API-key management). Streaming/binary and login-only endpoints are intentionally not exposed. +- **Disabled by default:** with `MCP_ENABLED` unset, the `/mcp` endpoint is not mounted and there is zero change to REST behavior. + +--- + ## 🛠 Tech Stack | Layer | Technology | diff --git a/package-lock.json b/package-lock.json index 89f318c2..e34cc498 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,8 @@ "@nestjs/swagger": "^11.4.4", "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", + "@silkweave/core": "^2.2.0", + "@silkweave/nestjs": "^2.2.0", "archiver": "^8.0.0", "bullmq": "^5.78.1", "class-transformer": "^0.5.1", @@ -759,7 +761,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1265,7 +1266,6 @@ "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-8.0.0.tgz", "integrity": "sha512-GYXJNJclgm9H5Tt/tmuNWfjyF2BuygMwl8xrRIfxxOTgcK4SB8zNUPveTcHGAOoKKtPDVotHNZCBRrFCcOAXMA==", "license": "MIT", - "peer": true, "dependencies": { "redis-info": "^3.1.0" }, @@ -1304,11 +1304,38 @@ "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-8.0.0.tgz", "integrity": "sha512-X/F256CmpBj9oj+fK2wOzjygkhTtjgWnc3f/SxPiUs3rQFvgYCplJLEaURh13J+Tpq46/1drAxq4Ukt4e5LelA==", "license": "MIT", - "peer": true, "dependencies": { "@bull-board/api": "8.0.0" } }, + "node_modules/@clack/core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.1.tgz", + "integrity": "sha512-FILJa1gGKEFTGZAJE9RpVhrjKz3c3h4ar60dSv6cGuDqufQ84YEIS3GAGvZiN+H6yaLbbvTFNejjCC4tXpZEuw==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.5.1.tgz", + "integrity": "sha512-zccHj2z2oCCO4yrDiRSlFOxWerGqRiysP7a5jPK6uoI9URKAquwY42Dd/iUP8JWHxEzdRe4TlbvZCo8z1/mhrw==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -1594,7 +1621,6 @@ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" @@ -1626,7 +1652,6 @@ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", @@ -1640,6 +1665,18 @@ "node": ">=6" } }, + "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.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -2784,6 +2821,68 @@ "integrity": "sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "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", + "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.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/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/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/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", @@ -2894,7 +2993,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.4.tgz", "integrity": "sha512-VBJcDHSAzxQnpcDfA0kt9MTGUD1XZzfByV70su0W0eDCQ9aqIEBlzWRW21tv9FG9dIut22ysgDidshdjlnczLw==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "2.8.1" }, @@ -3049,7 +3147,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.27.tgz", "integrity": "sha512-kEGSzqM2lWr4whh4Ubflw+oPZSEzxvRMu9WL+LveZploJWTjec5bBlCiRVlVzTPg2kIwBiLwWSvCCW7Wnin1gg==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.4", "iterare": "1.2.1", @@ -3096,7 +3193,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.27.tgz", "integrity": "sha512-K6DX7hcqmZdeXkv7tsPakKBRCgqL19a4mtbX4FluY0hWtFdtPKp6lbe+lb8gWPfvLdbOWr/CPScn7BSjBX+Ecg==", "license": "MIT", - "peer": true, "dependencies": { "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", @@ -3156,7 +3252,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.27.tgz", "integrity": "sha512-0ZFhz6H6EdGh4xQVbUNwjoAwBuz73P7FvUAl67h9CTdMqQlJDaQYJApBv8pKfVZ1fGjMCbl0m9DcC6pXaZPWSQ==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3178,7 +3273,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.27.tgz", "integrity": "sha512-xgpLzaIDGOCC6xOAtHnRAz8sqieFgGxxu3MN5ID026Jt6oeL3efp29N5QHhPr7UlqBfy/Jd02uj0POkZq6Au3Q==", "license": "MIT", - "peer": true, "dependencies": { "socket.io": "4.8.3", "tslib": "2.8.1" @@ -3364,7 +3458,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.1.tgz", "integrity": "sha512-8rw/nKT0S+L+MkzgE9F2/mox7mAgsPlwfzmW9gsESN1lmQtIrVEfiiBwC2O8+guS1jBfQehJIdcdUj2OAp4VUQ==", "license": "MIT", - "peer": true, "peerDependencies": { "@nestjs/common": "^10.0.0 || ^11.0.0", "@nestjs/core": "^10.0.0 || ^11.0.0", @@ -3374,9 +3467,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "11.1.19", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.19.tgz", - "integrity": "sha512-2qo8jtIwwwgkqAI1BtnJ02EaFLrRkKA39eYXS8IhZCHilhBHCWdjnJ5cLcFq4oF+s+KZ7LcLGD/3stxJy8ijzg==", + "version": "11.1.27", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.27.tgz", + "integrity": "sha512-X3OgJt9KgYTvt9D7sNz9SOj3A1daAHy7DZrYhM1pky8Fh+erlKQH5IQ/tKm+GaJKA5M0srBUr1CMqjak/qNxOw==", "license": "MIT", "peer": true, "dependencies": { @@ -3458,6 +3551,12 @@ "@noble/hashes": "^1.1.5" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3587,6 +3686,90 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@silkweave/auth": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@silkweave/auth/-/auth-2.2.0.tgz", + "integrity": "sha512-+OZpSomLkDW5km45AU3y2NoF0GwsIZf0GpsCRtgtGI2ss5Hjzm0YCz9MFzT+IiYDW+c9Hp0NPg81imclcW+P4g==", + "dependencies": { + "@silkweave/core": "2.2.0", + "jose": "^6.0.0" + } + }, + "node_modules/@silkweave/core": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@silkweave/core/-/core-2.2.0.tgz", + "integrity": "sha512-eluYDXVuP9udrTLqR33+kdkDgRG0pEvZvu7i15UuyfVsPt9Wbs8XGKnJm9JggtkgC9dkyRtShQwtITp8dupHQA==", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "@silkweave/logger": "2.2.0" + }, + "peerDependencies": { + "zod": "^3.25.0" + } + }, + "node_modules/@silkweave/logger": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@silkweave/logger/-/logger-2.2.0.tgz", + "integrity": "sha512-TH004iNZjkbIEgLNW1TDw40y1qi4eAfFh6k9JoR0tsJGUdEB2Wj3qFyOCW0nSoQ0qbd4kEROs3WIFQ5P1E6oLw==", + "dependencies": { + "@clack/prompts": "^1.0.1", + "pino": "^9.6.0", + "zod": "^3.25.0" + } + }, + "node_modules/@silkweave/mcp": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@silkweave/mcp/-/mcp-2.2.0.tgz", + "integrity": "sha512-wGrbzsL+wHTRgeOYuVTsJVIz7MFpuycAmc5T/iZq82BxteOotrClZ8e2eItcm1mROikCtRtCVzyfTUmKFaZQ0w==", + "dependencies": { + "@clack/prompts": "^1.0.1", + "@modelcontextprotocol/sdk": "^1.29.0", + "@silkweave/auth": "2.2.0", + "@silkweave/core": "2.2.0", + "@silkweave/logger": "2.2.0", + "change-case": "^5.4.4", + "commander": "^13.1.0", + "cors": "^2.8.6", + "express": "^5.2.1", + "zod": "^3.25.0" + } + }, + "node_modules/@silkweave/mcp/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@silkweave/nestjs": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@silkweave/nestjs/-/nestjs-2.2.0.tgz", + "integrity": "sha512-6xVet7twpT9PhVZLC0Vq64Lbb8P0IzLh0u8urkziiNQSsP6oCl4NNX9CYyALBhMIc8cHTwTgKZHKphRzgMErrA==", + "dependencies": { + "@silkweave/auth": "2.2.0", + "@silkweave/core": "2.2.0", + "@silkweave/mcp": "2.2.0", + "cors": "^2.8.6", + "express": "^5.2.1", + "zod": "^3.25.0" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "@nestjs/swagger": "^7.0.0 || ^8.0.0 || ^11.0.0", + "class-validator": "^0.14.0 || ^0.15.0" + }, + "peerDependenciesMeta": { + "@nestjs/swagger": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -3946,7 +4129,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4066,7 +4248,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } @@ -4292,7 +4473,6 @@ "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.61.0", "@typescript-eslint/types": "8.61.0", @@ -5006,7 +5186,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "devOptional": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5096,7 +5275,6 @@ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5112,7 +5290,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -5130,7 +5307,6 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -5147,7 +5323,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, "license": "MIT" }, "node_modules/ajv-keywords": { @@ -5517,6 +5692,15 @@ "dev": true, "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5656,7 +5840,6 @@ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -5959,7 +6142,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -6050,7 +6232,6 @@ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.78.1.tgz", "integrity": "sha512-zD5IT+qMqbMgPFPdL9FwnZka1bz6nckM+5lXj4N0vsXqdzoVO6wizmXpwsg/4GnHmXJsL7XOKeWA64tYUdPrOA==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.10.1", @@ -6313,6 +6494,12 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -6346,7 +6533,6 @@ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -6416,15 +6602,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -7273,8 +7457,7 @@ "version": "0.0.1581282", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dezalgo": { "version": "1.0.4", @@ -7773,7 +7956,6 @@ "integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==", "dev": true, "license": "MIT", - "peer": true, "workspaces": [ "packages/*" ], @@ -7833,7 +8015,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -8088,6 +8269,27 @@ "bare-events": "^2.7.0" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -8199,6 +8401,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "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" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -8238,7 +8458,6 @@ "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, "license": "MIT" }, "node_modules/fast-diff": { @@ -8274,11 +8493,25 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "dev": true, "funding": [ { "type": "github", @@ -8291,6 +8524,15 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fast-xml-builder": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", @@ -9090,6 +9332,15 @@ "url": "https://github.com/sponsors/EvanHahn" } }, + "node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "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", @@ -9311,7 +9562,6 @@ "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", "license": "MIT", - "peer": true, "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", @@ -9330,9 +9580,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "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" @@ -9594,7 +9844,6 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -10321,6 +10570,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", @@ -10372,6 +10630,12 @@ "dev": true, "license": "MIT" }, + "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", @@ -11520,6 +11784,7 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 6" } @@ -11536,6 +11801,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -11899,7 +12173,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", @@ -12003,6 +12276,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -12013,6 +12323,15 @@ "node": ">= 6" } }, + "node_modules/pkce-challenge": { + "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" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -12192,7 +12511,6 @@ "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -12260,6 +12578,22 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -12678,6 +13012,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -12847,6 +13187,15 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -12881,8 +13230,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/require-directory": { "version": "2.1.1", @@ -12897,7 +13245,6 @@ "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==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13032,7 +13379,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13057,6 +13403,15 @@ ], "license": "MIT" }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -13351,6 +13706,12 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -13484,6 +13845,15 @@ "node": ">= 10" } }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -13559,7 +13929,6 @@ "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", "hasInstallScript": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", @@ -14085,7 +14454,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -14239,6 +14607,15 @@ "b4a": "^1.6.4" } }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -14445,7 +14822,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -14630,7 +15006,6 @@ "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.29.tgz", "integrity": "sha512-wwPEX/df4l72gCmOsrs0otJZYLGA9lLQkUZCkukbsymEycV4zXv2KM7wU7v2r8L01TaCgY9ApSSqHQWBOUhEoQ==", "license": "MIT", - "peer": true, "dependencies": { "@sqltools/formatter": "^1.2.5", "ansis": "^4.2.0", @@ -14850,7 +15225,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15182,7 +15556,6 @@ "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -15251,7 +15624,6 @@ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -15800,6 +16172,15 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "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" + } } } } diff --git a/package.json b/package.json index 4004d6b3..a7d97f3a 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "@nestjs/swagger": "^11.4.4", "@nestjs/throttler": "^6.5.0", "@nestjs/typeorm": "^11.0.0", + "@silkweave/core": "^2.2.0", + "@silkweave/nestjs": "^2.2.0", "archiver": "^8.0.0", "bullmq": "^5.78.1", "class-transformer": "^0.5.1", @@ -110,6 +112,9 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, + "moduleNameMapper": { + "^@silkweave/nestjs$": "/../test/mocks/silkweave-nestjs.ts" + }, "collectCoverageFrom": [ "**/*.(t|j)s" ], diff --git a/src/app.module.ts b/src/app.module.ts index 7266e5ec..89a0abb2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,6 +29,7 @@ import { CatalogModule } from './modules/catalog/catalog.module'; import { HooksModule } from './core/hooks'; import { PluginsModule } from './core/plugins'; import { PluginsApiModule } from './modules/plugins/plugins.module'; +import { ApiKeyGuard } from './modules/auth/guards/api-key.guard'; // Only import QueueModule if explicitly enabled to avoid Redis connection errors const queueModules: Array = []; @@ -40,6 +41,24 @@ if (process.env.QUEUE_ENABLED === 'true') { queueModules.push(queueModule.QueueModule); } +// MCP is opt-in (off by default). Only when enabled do we load @silkweave/nestjs +// and register the MCP adapter; it reflects @Mcp()-decorated controller routes +// into MCP tools and mounts them at /mcp. `globalGuards: [ApiKeyGuard]` makes the +// existing global API-key auth run on tool calls (the app-global APP_GUARDs are not +// otherwise applied to Silkweave's raw routes). See @Mcp() usage in the controllers. +const mcpModules: Array = []; +if (process.env.MCP_ENABLED === 'true') { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { SilkweaveModule, mcp } = require('@silkweave/nestjs') as typeof import('@silkweave/nestjs'); + mcpModules.push( + SilkweaveModule.forRoot({ + silkweave: { name: 'openwa', description: 'OpenWA — self-hosted WhatsApp HTTP API', version: '0.2.3' }, + adapters: [mcp({ basePath: '/mcp' })], + globalGuards: [ApiKeyGuard], + }), + ); +} + @Module({ imports: [ // Configuration @@ -186,6 +205,7 @@ if (process.env.QUEUE_ENABLED === 'true') { StatusModule, // Phase 3: Status/Stories API CatalogModule, // Phase 3: Catalog API (WhatsApp Business) PluginsApiModule, // Phase 5: Plugins API + ...mcpModules, // Opt-in MCP server (MCP_ENABLED=true) — additive, reflects @Mcp() routes ], }) export class AppModule {} diff --git a/src/modules/audit/audit.controller.ts b/src/modules/audit/audit.controller.ts index 39b7a874..7945da6b 100644 --- a/src/modules/audit/audit.controller.ts +++ b/src/modules/audit/audit.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Query } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { AuditService, AuditQueryOptions } from './audit.service'; import { AuditLog, AuditAction, AuditSeverity } from './entities/audit-log.entity'; @@ -20,6 +21,7 @@ export class AuditController { status: 200, description: 'Paginated list of audit logs', }) + @Mcp() async findAll( @Query('action') action?: AuditAction, @Query('severity') severity?: AuditSeverity, diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index dc157802..5445f583 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Post, Put, Delete, Body, Param, HttpCode, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { AuthService } from './auth.service'; import { CreateApiKeyDto, UpdateApiKeyDto, ApiKeyResponseDto, ApiKeyCreatedResponseDto } from './dto'; import { RequireRole } from './decorators/auth.decorators'; @@ -18,6 +19,7 @@ export class AuthController { description: 'API key created', type: ApiKeyCreatedResponseDto, }) + @Mcp() async create(@Body() dto: CreateApiKeyDto): Promise { const { apiKey, rawKey } = await this.authService.createApiKey(dto); return { @@ -40,6 +42,7 @@ export class AuthController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'List all API keys (admin only)' }) @ApiResponse({ status: 200, type: [ApiKeyResponseDto] }) + @Mcp() async findAll(): Promise { const keys = await this.authService.findAll(); return keys.map(k => ({ @@ -61,6 +64,7 @@ export class AuthController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Get API key details (admin only)' }) @ApiResponse({ status: 200, type: ApiKeyResponseDto }) + @Mcp() async findOne(@Param('id') id: string): Promise { const k = await this.authService.findOne(id); return { @@ -82,6 +86,7 @@ export class AuthController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Update API key (admin only)' }) @ApiResponse({ status: 200, type: ApiKeyResponseDto }) + @Mcp() async update(@Param('id') id: string, @Body() dto: UpdateApiKeyDto): Promise { const k = await this.authService.update(id, dto); return { @@ -104,6 +109,7 @@ export class AuthController { @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Delete API key (admin only)' }) @ApiResponse({ status: 204, description: 'API key deleted' }) + @Mcp() async delete(@Param('id') id: string): Promise { await this.authService.delete(id); } @@ -112,6 +118,7 @@ export class AuthController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Revoke API key (admin only)' }) @ApiResponse({ status: 200, type: ApiKeyResponseDto }) + @Mcp() async revoke(@Param('id') id: string): Promise { const k = await this.authService.revoke(id); return { diff --git a/src/modules/catalog/catalog.controller.ts b/src/modules/catalog/catalog.controller.ts index d2ecf98e..94cf15c3 100644 --- a/src/modules/catalog/catalog.controller.ts +++ b/src/modules/catalog/catalog.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Post, Param, Body, Query } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { CatalogService } from './catalog.service'; import { SendProductDto, SendCatalogDto, ProductQueryDto } from './dto/send-product.dto'; @@ -10,30 +11,35 @@ export class CatalogController { @Get('catalog') @ApiOperation({ summary: 'Get business catalog info' }) + @Mcp() async getCatalog(@Param('sessionId') sessionId: string) { return this.catalogService.getCatalog(sessionId); } @Get('catalog/products') @ApiOperation({ summary: 'List catalog products' }) + @Mcp() async getProducts(@Param('sessionId') sessionId: string, @Query() query: ProductQueryDto) { return this.catalogService.getProducts(sessionId, query.page, query.limit); } @Get('catalog/products/:productId') @ApiOperation({ summary: 'Get a specific product' }) + @Mcp() async getProduct(@Param('sessionId') sessionId: string, @Param('productId') productId: string) { return this.catalogService.getProduct(sessionId, productId); } @Post('messages/send-product') @ApiOperation({ summary: 'Send a product message' }) + @Mcp() async sendProduct(@Param('sessionId') sessionId: string, @Body() dto: SendProductDto) { return this.catalogService.sendProduct(sessionId, dto.chatId, dto.productId, dto.body); } @Post('messages/send-catalog') @ApiOperation({ summary: 'Send catalog link' }) + @Mcp() async sendCatalog(@Param('sessionId') sessionId: string, @Body() dto: SendCatalogDto) { return this.catalogService.sendCatalog(sessionId, dto.chatId, dto.body); } diff --git a/src/modules/channel/channel.controller.ts b/src/modules/channel/channel.controller.ts index 734b01af..858e5ff1 100644 --- a/src/modules/channel/channel.controller.ts +++ b/src/modules/channel/channel.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Post, Delete, Param, Body, Query } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody, ApiQuery } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { ChannelService } from './channel.service'; import { SubscribeChannelDto } from './dto/subscribe-channel.dto'; @@ -16,6 +17,7 @@ export class ChannelController { description: 'List of subscribed channels', }) @ApiResponse({ status: 400, description: 'Session not ready' }) + @Mcp() async findAll(@Param('sessionId') sessionId: string) { return this.channelService.getSubscribedChannels(sessionId); } @@ -29,6 +31,7 @@ export class ChannelController { description: 'Channel details', }) @ApiResponse({ status: 404, description: 'Channel not found' }) + @Mcp() async findOne(@Param('sessionId') sessionId: string, @Param('channelId') channelId: string) { return this.channelService.getChannelById(sessionId, channelId); } @@ -42,6 +45,7 @@ export class ChannelController { status: 200, description: 'List of channel messages', }) + @Mcp() async getMessages( @Param('sessionId') sessionId: string, @Param('channelId') channelId: string, @@ -70,6 +74,7 @@ export class ChannelController { status: 201, description: 'Successfully subscribed to channel', }) + @Mcp() async subscribe(@Param('sessionId') sessionId: string, @Body() body: SubscribeChannelDto) { return this.channelService.subscribeToChannel(sessionId, body.inviteCode); } @@ -82,6 +87,7 @@ export class ChannelController { status: 200, description: 'Successfully unsubscribed from channel', }) + @Mcp() async unsubscribe(@Param('sessionId') sessionId: string, @Param('channelId') channelId: string) { await this.channelService.unsubscribeFromChannel(sessionId, channelId); return { success: true }; diff --git a/src/modules/contact/contact.controller.ts b/src/modules/contact/contact.controller.ts index 129ba266..ccad8998 100644 --- a/src/modules/contact/contact.controller.ts +++ b/src/modules/contact/contact.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Post, Delete, Param, HttpCode, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { ContactService } from './contact.service'; @ApiTags('contacts') @@ -16,6 +17,7 @@ export class ContactController { }) @ApiResponse({ status: 400, description: 'Session not ready' }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async findAll(@Param('sessionId') sessionId: string) { return this.contactService.getContacts(sessionId); } @@ -29,6 +31,7 @@ export class ContactController { description: 'Contact details', }) @ApiResponse({ status: 404, description: 'Contact not found' }) + @Mcp() async findOne(@Param('sessionId') sessionId: string, @Param('contactId') contactId: string) { return this.contactService.getContactById(sessionId, contactId); } @@ -41,6 +44,7 @@ export class ContactController { status: 200, description: 'Number existence check result', }) + @Mcp() async checkNumber(@Param('sessionId') sessionId: string, @Param('number') number: string) { const exists = await this.contactService.checkNumberExists(sessionId, number); return { @@ -60,6 +64,7 @@ export class ContactController { status: 200, description: 'Profile picture URL', }) + @Mcp() async getProfilePicture(@Param('sessionId') sessionId: string, @Param('contactId') contactId: string) { const url = await this.contactService.getProfilePicture(sessionId, contactId); return { url }; @@ -74,6 +79,7 @@ export class ContactController { status: 200, description: 'Contact blocked', }) + @Mcp() async blockContact(@Param('sessionId') sessionId: string, @Param('contactId') contactId: string) { await this.contactService.blockContact(sessionId, contactId); return { success: true, message: 'Contact blocked' }; @@ -87,6 +93,7 @@ export class ContactController { status: 200, description: 'Contact unblocked', }) + @Mcp() async unblockContact(@Param('sessionId') sessionId: string, @Param('contactId') contactId: string) { await this.contactService.unblockContact(sessionId, contactId); return { success: true, message: 'Contact unblocked' }; diff --git a/src/modules/group/group.controller.ts b/src/modules/group/group.controller.ts index 81ce054d..4dff8d76 100644 --- a/src/modules/group/group.controller.ts +++ b/src/modules/group/group.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Post, Put, Delete, Param, Body, HttpCode, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { GroupService } from './group.service'; import { CreateGroupDto, ParticipantsDto, GroupSubjectDto, GroupDescriptionDto } from './dto/group.dto'; @@ -12,6 +13,7 @@ export class GroupController { @ApiOperation({ summary: 'Get all groups for a session' }) @ApiParam({ name: 'sessionId', description: 'Session ID' }) @ApiResponse({ status: 200, description: 'List of groups' }) + @Mcp() async findAll(@Param('sessionId') sessionId: string) { return this.groupService.getGroups(sessionId); } @@ -22,6 +24,7 @@ export class GroupController { @ApiParam({ name: 'groupId', description: 'Group ID (e.g., 120363xxx@g.us)' }) @ApiResponse({ status: 200, description: 'Group details with participants' }) @ApiResponse({ status: 404, description: 'Group not found' }) + @Mcp() async findOne(@Param('sessionId') sessionId: string, @Param('groupId') groupId: string) { return this.groupService.getGroupInfo(sessionId, groupId); } @@ -31,6 +34,7 @@ export class GroupController { @ApiParam({ name: 'sessionId', description: 'Session ID' }) @ApiBody({ type: CreateGroupDto }) @ApiResponse({ status: 201, description: 'Group created' }) + @Mcp() async create(@Param('sessionId') sessionId: string, @Body() dto: CreateGroupDto) { return this.groupService.createGroup(sessionId, dto.name, dto.participants); } @@ -42,6 +46,7 @@ export class GroupController { @ApiBody({ type: ParticipantsDto }) @ApiResponse({ status: 200, description: 'Participants added' }) @HttpCode(HttpStatus.OK) + @Mcp() async addParticipants( @Param('sessionId') sessionId: string, @Param('groupId') groupId: string, @@ -57,6 +62,7 @@ export class GroupController { @ApiParam({ name: 'groupId', description: 'Group ID' }) @ApiBody({ type: ParticipantsDto }) @ApiResponse({ status: 200, description: 'Participants removed' }) + @Mcp() async removeParticipants( @Param('sessionId') sessionId: string, @Param('groupId') groupId: string, @@ -73,6 +79,7 @@ export class GroupController { @ApiBody({ type: ParticipantsDto }) @ApiResponse({ status: 200, description: 'Participants promoted' }) @HttpCode(HttpStatus.OK) + @Mcp() async promoteParticipants( @Param('sessionId') sessionId: string, @Param('groupId') groupId: string, @@ -89,6 +96,7 @@ export class GroupController { @ApiBody({ type: ParticipantsDto }) @ApiResponse({ status: 200, description: 'Participants demoted' }) @HttpCode(HttpStatus.OK) + @Mcp() async demoteParticipants( @Param('sessionId') sessionId: string, @Param('groupId') groupId: string, @@ -104,6 +112,7 @@ export class GroupController { @ApiParam({ name: 'groupId', description: 'Group ID' }) @ApiBody({ type: GroupSubjectDto }) @ApiResponse({ status: 200, description: 'Subject updated' }) + @Mcp() async setSubject( @Param('sessionId') sessionId: string, @Param('groupId') groupId: string, @@ -119,6 +128,7 @@ export class GroupController { @ApiParam({ name: 'groupId', description: 'Group ID' }) @ApiBody({ type: GroupDescriptionDto }) @ApiResponse({ status: 200, description: 'Description updated' }) + @Mcp() async setDescription( @Param('sessionId') sessionId: string, @Param('groupId') groupId: string, @@ -134,6 +144,7 @@ export class GroupController { @ApiParam({ name: 'groupId', description: 'Group ID' }) @ApiResponse({ status: 200, description: 'Left the group' }) @HttpCode(HttpStatus.OK) + @Mcp() async leave(@Param('sessionId') sessionId: string, @Param('groupId') groupId: string) { await this.groupService.leaveGroup(sessionId, groupId); return { success: true, message: 'Left the group' }; @@ -146,6 +157,7 @@ export class GroupController { @ApiParam({ name: 'sessionId', description: 'Session ID' }) @ApiParam({ name: 'groupId', description: 'Group ID' }) @ApiResponse({ status: 200, description: 'Group invite code' }) + @Mcp() async getInviteCode(@Param('sessionId') sessionId: string, @Param('groupId') groupId: string) { const inviteCode = await this.groupService.getGroupInviteCode(sessionId, groupId); return { @@ -160,6 +172,7 @@ export class GroupController { @ApiParam({ name: 'sessionId', description: 'Session ID' }) @ApiParam({ name: 'groupId', description: 'Group ID' }) @ApiResponse({ status: 200, description: 'New invite code generated' }) + @Mcp() async revokeInviteCode(@Param('sessionId') sessionId: string, @Param('groupId') groupId: string) { const newCode = await this.groupService.revokeGroupInviteCode(sessionId, groupId); return { diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts index 0bd97114..e2c868d1 100644 --- a/src/modules/health/health.controller.ts +++ b/src/modules/health/health.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, ServiceUnavailableException } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; +import { Mcp } from '@silkweave/nestjs'; import { Public } from '../auth/decorators/auth.decorators'; import { SkipThrottle } from '@nestjs/throttler'; import { ShutdownService } from '../../common/services/shutdown.service'; @@ -32,6 +33,7 @@ export class HealthController { @Get() @ApiOperation({ summary: 'Basic health check' }) @ApiResponse({ status: 200, description: 'Application is healthy' }) + @Mcp() check(): { status: string; timestamp: string } { return { status: 'ok', @@ -42,6 +44,7 @@ export class HealthController { @Get('live') @ApiOperation({ summary: 'Liveness probe for Kubernetes' }) @ApiResponse({ status: 200, description: 'Application is alive' }) + @Mcp() liveness(): { status: string } { // Liveness only reflects process liveness — deliberately static so a transient // dependency outage doesn't trigger a pod KILL (that's readiness' job). @@ -52,6 +55,7 @@ export class HealthController { @ApiOperation({ summary: 'Readiness probe — verifies the auth/audit + data databases respond' }) @ApiResponse({ status: 200, description: 'Application is ready to accept traffic' }) @ApiResponse({ status: 503, description: 'A required dependency is down' }) + @Mcp() async readiness(): Promise { // While draining (shutdown started), report 503 so the LB/orchestrator stops // routing new traffic before teardown — even if the DBs are still up. diff --git a/src/modules/infra/infra.controller.ts b/src/modules/infra/infra.controller.ts index 179f8cff..2cb580bf 100644 --- a/src/modules/infra/infra.controller.ts +++ b/src/modules/infra/infra.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Put, Post, Body, BadRequestException } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { ConfigService } from '@nestjs/config'; import { DataSource } from 'typeorm'; import { InjectDataSource } from '@nestjs/typeorm'; @@ -186,6 +187,7 @@ export class InfraController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Get infrastructure status' }) @ApiResponse({ status: 200, description: 'Infrastructure status' }) + @Mcp() async getStatus(): Promise { // Check both database connections const mainDbConnected = this.mainDataSource.isInitialized; @@ -227,6 +229,7 @@ export class InfraController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Get available WhatsApp engines' }) @ApiResponse({ status: 200, description: 'List of available engines' }) + @Mcp() getEngines(): Array<{ id: string; name: string; enabled: boolean; features: string[] }> { return this.engineFactory.getAvailableEngines(); } @@ -235,6 +238,7 @@ export class InfraController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Get current active engine' }) @ApiResponse({ status: 200, description: 'Current engine info' }) + @Mcp() getCurrentEngine(): { engineType: string } { return { engineType: this.engineFactory.getCurrentEngine() }; } @@ -243,6 +247,7 @@ export class InfraController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Read the saved infrastructure configuration for the dashboard form' }) @ApiResponse({ status: 200, description: 'Saved configuration (secrets omitted)' }) + @Mcp() getConfig(): SavedConfigResponse { const envPath = path.resolve(process.cwd(), 'data', '.env.generated'); const saved: Record = fs.existsSync(envPath) ? dotenv.parse(fs.readFileSync(envPath, 'utf8')) : {}; @@ -293,6 +298,7 @@ export class InfraController { @ApiOperation({ summary: 'Save infrastructure configuration to .env file' }) @ApiResponse({ status: 200, description: 'Configuration saved' }) @ApiBody({ description: 'Configuration to save' }) + @Mcp() saveConfig(@Body() config: SaveConfigDto): { message: string; saved: boolean; envPath: string; profiles: string[] } { try { const profiles: string[] = []; @@ -468,6 +474,7 @@ export class InfraController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Request server restart with Docker orchestration' }) @ApiResponse({ status: 200, description: 'Server will restart with new profiles' }) + @Mcp() async requestRestart(@Body() body?: { profiles?: string[]; profilesToRemove?: string[] }): Promise<{ message: string; restarting: boolean; @@ -560,6 +567,7 @@ export class InfraController { @Public() @ApiOperation({ summary: 'Health check endpoint' }) @ApiResponse({ status: 200, description: 'Server is healthy' }) + @Mcp() healthCheck(): { status: string; timestamp: string } { return { status: 'ok', @@ -571,6 +579,7 @@ export class InfraController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Export all data from Data DB for migration' }) @ApiResponse({ status: 200, description: 'Exported data as JSON' }) + @Mcp() async exportData(): Promise<{ exportedAt: string; dataDbType: string; @@ -636,6 +645,7 @@ export class InfraController { }, }) @ApiResponse({ status: 200, description: 'Data imported successfully' }) + @Mcp() async importData( @Body() data: { @@ -805,6 +815,7 @@ export class InfraController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Get file count in current storage' }) @ApiResponse({ status: 200, description: 'File count and size' }) + @Mcp() async getStorageFileCount(): Promise<{ storageType: string; count: number; @@ -824,6 +835,7 @@ export class InfraController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Export all storage files as tar.gz' }) @ApiResponse({ status: 200, description: 'Tar.gz archive stream' }) + @Mcp() async exportStorage(): Promise<{ message: string; download: string }> { // Note: In production, this would return a StreamableFile // For simplicity, we'll save to a temp file and return the path @@ -849,6 +861,7 @@ export class InfraController { @ApiOperation({ summary: 'Import storage files from tar.gz' }) @ApiBody({ description: 'Path to tar.gz file to import' }) @ApiResponse({ status: 200, description: 'Import result' }) + @Mcp() async importStorage( @Body() body: { filePath: string }, ): Promise<{ imported: boolean; count: number; storageType: string }> { diff --git a/src/modules/label/label.controller.ts b/src/modules/label/label.controller.ts index d78a7e01..06e55469 100644 --- a/src/modules/label/label.controller.ts +++ b/src/modules/label/label.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Post, Delete, Param, Body } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { LabelService } from './label.service'; import { AddLabelDto } from './dto/add-label.dto'; @@ -17,6 +18,7 @@ export class LabelController { }) @ApiResponse({ status: 400, description: 'Session not ready or not a business account' }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async findAll(@Param('sessionId') sessionId: string) { return this.labelService.getLabels(sessionId); } @@ -30,6 +32,7 @@ export class LabelController { description: 'Label details', }) @ApiResponse({ status: 404, description: 'Label not found' }) + @Mcp() async findOne(@Param('sessionId') sessionId: string, @Param('labelId') labelId: string) { return this.labelService.getLabelById(sessionId, labelId); } @@ -42,6 +45,7 @@ export class LabelController { status: 200, description: 'List of labels for the chat', }) + @Mcp() async getChatLabels(@Param('sessionId') sessionId: string, @Param('chatId') chatId: string) { return this.labelService.getChatLabels(sessionId, chatId); } @@ -63,6 +67,7 @@ export class LabelController { status: 200, description: 'Label added to chat', }) + @Mcp() async addLabelToChat( @Param('sessionId') sessionId: string, @Param('chatId') chatId: string, @@ -81,6 +86,7 @@ export class LabelController { status: 200, description: 'Label removed from chat', }) + @Mcp() async removeLabelFromChat( @Param('sessionId') sessionId: string, @Param('chatId') chatId: string, diff --git a/src/modules/message/message.controller.ts b/src/modules/message/message.controller.ts index 4cf7fcb4..d6d33c2b 100644 --- a/src/modules/message/message.controller.ts +++ b/src/modules/message/message.controller.ts @@ -1,5 +1,6 @@ import { Controller, Post, Get, Param, Body, Query, HttpCode, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { MessageService } from './message.service'; import { BulkMessageService } from './bulk-message.service'; import { SendTextMessageDto, SendMediaMessageDto, MessageResponseDto } from './dto'; @@ -34,6 +35,7 @@ export class MessageController { status: 200, description: 'Message history', }) + @Mcp() async getMessages( @Param('sessionId') sessionId: string, @Query('chatId') chatId?: string, @@ -61,6 +63,7 @@ export class MessageController { description: 'Session not active or invalid request', }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async sendText(@Param('sessionId') sessionId: string, @Body() dto: SendTextMessageDto): Promise { return this.messageService.sendText(sessionId, dto); } @@ -79,6 +82,7 @@ export class MessageController { description: 'Session not active or invalid request', }) @ApiResponse({ status: 404, description: 'Session or template not found' }) + @Mcp() async sendTemplate( @Param('sessionId') sessionId: string, @Body() dto: SendTemplateMessageDto, @@ -99,6 +103,7 @@ export class MessageController { status: 400, description: 'Session not active or invalid request', }) + @Mcp() async sendImage( @Param('sessionId') sessionId: string, @Body() dto: SendMediaMessageDto, @@ -119,6 +124,7 @@ export class MessageController { status: 400, description: 'Session not active or invalid request', }) + @Mcp() async sendVideo( @Param('sessionId') sessionId: string, @Body() dto: SendMediaMessageDto, @@ -139,6 +145,7 @@ export class MessageController { status: 400, description: 'Session not active or invalid request', }) + @Mcp() async sendAudio( @Param('sessionId') sessionId: string, @Body() dto: SendMediaMessageDto, @@ -159,6 +166,7 @@ export class MessageController { status: 400, description: 'Session not active or invalid request', }) + @Mcp() async sendDocument( @Param('sessionId') sessionId: string, @Body() dto: SendMediaMessageDto, @@ -177,6 +185,7 @@ export class MessageController { description: 'Location sent', type: MessageResponseDto, }) + @Mcp() async sendLocation(@Param('sessionId') sessionId: string, @Body() dto: SendLocationDto): Promise { return this.messageService.sendLocation(sessionId, dto); } @@ -190,6 +199,7 @@ export class MessageController { description: 'Contact sent', type: MessageResponseDto, }) + @Mcp() async sendContact(@Param('sessionId') sessionId: string, @Body() dto: SendContactDto): Promise { return this.messageService.sendContact(sessionId, dto); } @@ -203,6 +213,7 @@ export class MessageController { description: 'Sticker sent', type: MessageResponseDto, }) + @Mcp() async sendSticker( @Param('sessionId') sessionId: string, @Body() dto: SendMediaMessageDto, @@ -219,6 +230,7 @@ export class MessageController { description: 'Reply sent', type: MessageResponseDto, }) + @Mcp() async reply(@Param('sessionId') sessionId: string, @Body() dto: ReplyMessageDto): Promise { return this.messageService.reply(sessionId, dto); } @@ -232,6 +244,7 @@ export class MessageController { description: 'Message forwarded', type: MessageResponseDto, }) + @Mcp() async forward(@Param('sessionId') sessionId: string, @Body() dto: ForwardMessageDto): Promise { return this.messageService.forward(sessionId, dto); } @@ -251,6 +264,7 @@ export class MessageController { status: 400, description: 'Session not active or message not found', }) + @Mcp() async react(@Param('sessionId') sessionId: string, @Body() dto: ReactMessageDto): Promise<{ success: boolean }> { await this.messageService.reactToMessage(sessionId, dto); return { success: true }; @@ -273,6 +287,7 @@ export class MessageController { description: 'When true, downloads media (base64) for messages that have it. Slower; default false.', }) @ApiResponse({ status: 200, description: 'Chat history (most recent messages)' }) + @Mcp() async getChatHistory( @Param('sessionId') sessionId: string, @Param('chatId') chatId: string, @@ -299,6 +314,7 @@ export class MessageController { status: 200, description: 'List of reactions with senders', }) + @Mcp() async getReactions( @Param('sessionId') sessionId: string, @Param('chatId') chatId: string, @@ -322,6 +338,7 @@ export class MessageController { status: 400, description: 'Session not active or message not found', }) + @Mcp() async deleteMessage( @Param('sessionId') sessionId: string, @Body() dto: DeleteMessageDto, @@ -346,6 +363,7 @@ export class MessageController { status: 400, description: 'Session not active or invalid request', }) + @Mcp() async sendBulk( @Param('sessionId') sessionId: string, @Body() dto: SendBulkMessageDto, @@ -374,6 +392,7 @@ export class MessageController { status: 404, description: 'Batch not found', }) + @Mcp() async getBatchStatus(@Param('sessionId') sessionId: string, @Param('batchId') batchId: string) { const batch = await this.bulkMessageService.getBatchStatus(sessionId, batchId); return { @@ -404,6 +423,7 @@ export class MessageController { status: 404, description: 'Batch not found', }) + @Mcp() async cancelBatch(@Param('sessionId') sessionId: string, @Param('batchId') batchId: string) { const batch = await this.bulkMessageService.cancelBatch(sessionId, batchId); return { diff --git a/src/modules/plugins/plugins.controller.ts b/src/modules/plugins/plugins.controller.ts index 70f617a1..9cc58bc7 100644 --- a/src/modules/plugins/plugins.controller.ts +++ b/src/modules/plugins/plugins.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Post, Put, Param, Body, HttpCode, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { PluginsService } from './plugins.service'; import { PluginDto, PluginConfigDto } from './dto/plugin.dto'; import { RequireRole } from '../auth/decorators/auth.decorators'; @@ -13,6 +14,7 @@ export class PluginsController { @Get() @ApiOperation({ summary: 'List all plugins' }) @ApiResponse({ status: 200, description: 'List of all plugins' }) + @Mcp() findAll(): PluginDto[] { return this.pluginsService.findAll(); } @@ -21,6 +23,7 @@ export class PluginsController { @ApiOperation({ summary: 'Get plugin by ID' }) @ApiResponse({ status: 200, description: 'Plugin details' }) @ApiResponse({ status: 404, description: 'Plugin not found' }) + @Mcp() findOne(@Param('id') id: string): PluginDto { return this.pluginsService.findOne(id); } @@ -30,6 +33,7 @@ export class PluginsController { @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Enable a plugin' }) @ApiResponse({ status: 200, description: 'Plugin enabled successfully' }) + @Mcp() async enable(@Param('id') id: string): Promise<{ success: boolean; message: string }> { return await this.pluginsService.enable(id); } @@ -39,6 +43,7 @@ export class PluginsController { @HttpCode(HttpStatus.OK) @ApiOperation({ summary: 'Disable a plugin' }) @ApiResponse({ status: 200, description: 'Plugin disabled successfully' }) + @Mcp() async disable(@Param('id') id: string): Promise<{ success: boolean; message: string }> { return await this.pluginsService.disable(id); } @@ -47,6 +52,7 @@ export class PluginsController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Update plugin configuration' }) @ApiResponse({ status: 200, description: 'Plugin configuration updated' }) + @Mcp() updateConfig(@Param('id') id: string, @Body() configDto: PluginConfigDto): { success: boolean; message: string } { return this.pluginsService.updateConfig(id, configDto.config); } @@ -54,6 +60,7 @@ export class PluginsController { @Get(':id/health') @ApiOperation({ summary: 'Check plugin health' }) @ApiResponse({ status: 200, description: 'Plugin health status' }) + @Mcp() async healthCheck(@Param('id') id: string): Promise<{ healthy: boolean; message?: string }> { return await this.pluginsService.healthCheck(id); } diff --git a/src/modules/session/session.controller.ts b/src/modules/session/session.controller.ts index b435f4e1..18f17992 100644 --- a/src/modules/session/session.controller.ts +++ b/src/modules/session/session.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Post, Delete, Param, Body, HttpCode, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { SessionService } from './session.service'; import { CreateSessionDto, @@ -49,6 +50,7 @@ export class SessionController { type: SessionResponseDto, }) @ApiResponse({ status: 409, description: 'Session name already exists' }) + @Mcp() async create(@Body() dto: CreateSessionDto): Promise { const session = await this.sessionService.create(dto); await this.auditService.logInfo(AuditAction.SESSION_CREATED, { @@ -65,6 +67,7 @@ export class SessionController { description: 'List of sessions', type: [SessionResponseDto], }) + @Mcp() async findAll(): Promise { const sessions = await this.sessionService.findAll(); return sessions.map(s => this.transformSession(s)); @@ -79,6 +82,7 @@ export class SessionController { type: SessionResponseDto, }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async findOne(@Param('id') id: string): Promise { const session = await this.sessionService.findOne(id); return this.transformSession(session); @@ -91,6 +95,7 @@ export class SessionController { @ApiParam({ name: 'id', description: 'Session ID' }) @ApiResponse({ status: 204, description: 'Session deleted' }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async delete(@Param('id') id: string): Promise { const session = await this.sessionService.findOne(id); await this.sessionService.delete(id); @@ -113,6 +118,7 @@ export class SessionController { }) @ApiResponse({ status: 400, description: 'Session already started' }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async start(@Param('id') id: string): Promise { const session = await this.sessionService.start(id); await this.auditService.logInfo(AuditAction.SESSION_STARTED, { @@ -132,6 +138,7 @@ export class SessionController { type: SessionResponseDto, }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async stop(@Param('id') id: string): Promise { const session = await this.sessionService.stop(id); await this.auditService.logInfo(AuditAction.SESSION_STOPPED, { @@ -155,6 +162,7 @@ export class SessionController { description: 'QR code not ready or session already authenticated', }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async getQRCode(@Param('id') id: string): Promise { const qrCode = await this.sessionService.getQRCode(id); await this.auditService.logInfo(AuditAction.SESSION_QR_GENERATED, { @@ -186,6 +194,7 @@ export class SessionController { }) @ApiResponse({ status: 400, description: 'Session not ready' }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async getGroups(@Param('id') id: string): Promise<{ id: string; name: string; linkedParentJID?: string | null }[]> { return this.sessionService.getGroups(id); } @@ -196,6 +205,7 @@ export class SessionController { @ApiResponse({ status: 200, description: 'List of active chats' }) @ApiResponse({ status: 400, description: 'Session not ready' }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async getChats(@Param('id') id: string): Promise { return this.sessionService.getChats(id); } @@ -207,6 +217,7 @@ export class SessionController { @ApiResponse({ status: 200, description: 'Chat marked as read successfully' }) @ApiResponse({ status: 400, description: 'Session not ready' }) @ApiResponse({ status: 404, description: 'Session not found' }) + @Mcp() async markChatRead(@Param('id') id: string, @Body() dto: MarkChatReadDto): Promise<{ success: boolean }> { const success = await this.sessionService.sendSeen(id, dto.chatId); return { success }; @@ -220,6 +231,7 @@ export class SessionController { status: 200, description: 'Session statistics including counts and memory usage', }) + @Mcp() async getStats(): Promise<{ total: number; active: number; diff --git a/src/modules/settings/settings.controller.ts b/src/modules/settings/settings.controller.ts index d0c13cb9..523712fd 100644 --- a/src/modules/settings/settings.controller.ts +++ b/src/modules/settings/settings.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Put, Body } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; +import { Mcp } from '@silkweave/nestjs'; import { RequireRole } from '../auth/decorators/auth.decorators'; import { ApiKeyRole } from '../auth/entities/api-key.entity'; @@ -55,6 +56,7 @@ export class SettingsController { @Get() @ApiOperation({ summary: 'Get application settings' }) @ApiResponse({ status: 200, description: 'Current settings' }) + @Mcp() get(): Settings { return this.settings; } @@ -63,6 +65,7 @@ export class SettingsController { @RequireRole(ApiKeyRole.ADMIN) @ApiOperation({ summary: 'Update application settings' }) @ApiResponse({ status: 200, description: 'Settings updated' }) + @Mcp() update(@Body() newSettings: Partial): Settings { if (newSettings.general) { this.settings.general = { diff --git a/src/modules/stats/stats.controller.ts b/src/modules/stats/stats.controller.ts index edde5446..c676a031 100644 --- a/src/modules/stats/stats.controller.ts +++ b/src/modules/stats/stats.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { StatsService } from './stats.service'; import { StatsQueryDto } from './dto/stats-query.dto'; @@ -10,18 +11,21 @@ export class StatsController { @Get('overview') @ApiOperation({ summary: 'Get overall statistics' }) + @Mcp() async getOverview() { return this.statsService.getOverview(); } @Get('messages') @ApiOperation({ summary: 'Get message statistics with time series' }) + @Mcp() async getMessageStats(@Query() query: StatsQueryDto) { return this.statsService.getMessageStats(query.period || '24h'); } @Get('sessions/:sessionId') @ApiOperation({ summary: 'Get statistics for a specific session' }) + @Mcp() async getSessionStats(@Param('sessionId') sessionId: string) { return this.statsService.getSessionStats(sessionId); } diff --git a/src/modules/status/status.controller.ts b/src/modules/status/status.controller.ts index 5840695e..3987d2e0 100644 --- a/src/modules/status/status.controller.ts +++ b/src/modules/status/status.controller.ts @@ -1,5 +1,6 @@ import { Controller, Get, Post, Delete, Param, Body } from '@nestjs/common'; import { ApiTags, ApiOperation } from '@nestjs/swagger'; +import { Mcp } from '@silkweave/nestjs'; import { StatusService } from './status.service'; import { SendTextStatusDto } from './dto/send-text-status.dto'; import { SendImageStatusDto, SendVideoStatusDto } from './dto/send-media-status.dto'; @@ -11,18 +12,21 @@ export class StatusController { @Get() @ApiOperation({ summary: 'Get all contact status updates' }) + @Mcp() async getStatuses(@Param('sessionId') sessionId: string) { return { statuses: await this.statusService.getStatuses(sessionId) }; } @Get(':contactId') @ApiOperation({ summary: 'Get status updates from a specific contact' }) + @Mcp() async getContactStatus(@Param('sessionId') sessionId: string, @Param('contactId') contactId: string) { return { statuses: await this.statusService.getContactStatus(sessionId, contactId) }; } @Post('send-text') @ApiOperation({ summary: 'Post a text status' }) + @Mcp() async sendTextStatus(@Param('sessionId') sessionId: string, @Body() dto: SendTextStatusDto) { return this.statusService.postTextStatus(sessionId, dto.text, { backgroundColor: dto.backgroundColor, @@ -32,18 +36,21 @@ export class StatusController { @Post('send-image') @ApiOperation({ summary: 'Post an image status' }) + @Mcp() async sendImageStatus(@Param('sessionId') sessionId: string, @Body() dto: SendImageStatusDto) { return this.statusService.postImageStatus(sessionId, dto.image, dto.caption); } @Post('send-video') @ApiOperation({ summary: 'Post a video status' }) + @Mcp() async sendVideoStatus(@Param('sessionId') sessionId: string, @Body() dto: SendVideoStatusDto) { return this.statusService.postVideoStatus(sessionId, dto.video, dto.caption); } @Delete(':statusId') @ApiOperation({ summary: 'Delete own status' }) + @Mcp() async deleteStatus(@Param('sessionId') sessionId: string, @Param('statusId') statusId: string) { await this.statusService.deleteStatus(sessionId, statusId); return { message: 'Status deleted successfully' }; diff --git a/src/modules/template/template.controller.ts b/src/modules/template/template.controller.ts index 17e2a478..ca50adeb 100644 --- a/src/modules/template/template.controller.ts +++ b/src/modules/template/template.controller.ts @@ -5,6 +5,7 @@ import { CreateTemplateDto, UpdateTemplateDto, TemplateResponseDto } from './dto import { Template } from './entities/template.entity'; import { RequireRole } from '../auth/decorators/auth.decorators'; import { ApiKeyRole } from '../auth/entities/api-key.entity'; +import { Mcp } from '@silkweave/nestjs'; @ApiTags('templates') @Controller('sessions/:sessionId/templates') @@ -16,6 +17,7 @@ export class TemplateController { @ApiOperation({ summary: 'Create a message template for the session' }) @ApiParam({ name: 'sessionId', description: 'Session ID' }) @ApiResponse({ status: 201, description: 'Template created', type: TemplateResponseDto }) + @Mcp() async create(@Param('sessionId') sessionId: string, @Body() dto: CreateTemplateDto): Promise